Compare commits
2 Commits
16e9d663f4
...
v0.1.5
Author | SHA1 | Date | |
---|---|---|---|
a6d97b4a6f | |||
53ece08ba8 |
@ -18,5 +18,10 @@ server {
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
send_timeout 3600s;
|
||||||
|
proxy_buffering off;
|
||||||
|
gzip off;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {message} from "../hook/message/u-message.tsx";
|
import {message} from "../component/message/u-message.tsx";
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
|
||||||
|
|
||||||
export interface Prop {
|
|
||||||
/** 事件处理函数(可选) */
|
|
||||||
fn?: (event: MessageEvent) => Promise<void>;
|
|
||||||
/** 最大重试次数(可选) */
|
|
||||||
retry?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useWebsocket = (prop?: Prop) => {
|
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
|
||||||
const retryCountRef = useRef(0);
|
|
||||||
const reconnectTimerRef = useRef<number>(0);
|
|
||||||
const currentPropRef = useRef(prop);
|
|
||||||
|
|
||||||
// 更新最新 prop
|
|
||||||
currentPropRef.current = prop;
|
|
||||||
|
|
||||||
const connect = useCallback((url: string, connectProp?: Prop) => {
|
|
||||||
// 合并 prop 优先级:connectProp > hook prop
|
|
||||||
const mergedProp = { ...currentPropRef.current, ...connectProp };
|
|
||||||
|
|
||||||
// 清理现有连接
|
|
||||||
if (wsRef.current) {
|
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
clearTimeout(reconnectTimerRef.current);
|
|
||||||
|
|
||||||
const createConnection = () => {
|
|
||||||
const ws = new WebSocket(url);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
retryCountRef.current = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
mergedProp?.fn?.(event).catch(error => {
|
|
||||||
console.error('WebSocket message handler error:', error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
|
||||||
const maxRetries = mergedProp?.retry ?? 0;
|
|
||||||
|
|
||||||
if (!event.wasClean && retryCountRef.current < maxRetries) {
|
|
||||||
retryCountRef.current += 1;
|
|
||||||
const retryDelay = Math.pow(2, retryCountRef.current) * 1000;
|
|
||||||
|
|
||||||
reconnectTimerRef.current = setTimeout(() => {
|
|
||||||
createConnection();
|
|
||||||
}, retryDelay);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
ws.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
wsRef.current = ws;
|
|
||||||
};
|
|
||||||
|
|
||||||
createConnection();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const close = useCallback(() => {
|
|
||||||
wsRef.current?.close();
|
|
||||||
clearTimeout(reconnectTimerRef.current);
|
|
||||||
retryCountRef.current = currentPropRef.current?.retry || 0;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
close
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,6 +0,0 @@
|
|||||||
export interface Resp<T>{
|
|
||||||
status: number;
|
|
||||||
msg: string;
|
|
||||||
err: string;
|
|
||||||
data: T;
|
|
||||||
}
|
|
@ -1,21 +1,19 @@
|
|||||||
// import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
import {createBrowserRouter, RouterProvider} from "react-router-dom";
|
||||||
import {Login} from "./page/login.tsx";
|
import {Login} from "./page/login.tsx";
|
||||||
import {FileSharing} from "./page/share.tsx";
|
import {FileSharing} from "./page/share.tsx";
|
||||||
import {LocalSharing} from "./page/local.tsx";
|
|
||||||
|
|
||||||
const container = document.getElementById('root')
|
const container = document.getElementById('root')
|
||||||
const root = createRoot(container!)
|
const root = createRoot(container!)
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{path: "/login", element: <Login />},
|
{path: "/login", element: <Login />},
|
||||||
{path: "/share", element: <FileSharing />},
|
{path: "*", element: <FileSharing />},
|
||||||
{path: "*", element: <LocalSharing />},
|
|
||||||
])
|
])
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
// <StrictMode>
|
<StrictMode>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
// </StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@ import {createUseStyles} from "react-jss";
|
|||||||
import {UButton} from "../../component/button/u-button.tsx";
|
import {UButton} from "../../component/button/u-button.tsx";
|
||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {useStore} from "../../store/share.ts";
|
import {useStore} from "../../store/share.ts";
|
||||||
import {message} from "../../hook/message/u-message.tsx";
|
import {message} from "../../component/message/u-message.tsx";
|
||||||
import {useFileUpload} from "../../api/upload.ts";
|
import {useFileUpload} from "../../api/upload.ts";
|
||||||
|
|
||||||
const useUploadStyle = createUseStyles({
|
const useUploadStyle = createUseStyles({
|
||||||
|
@ -1,259 +0,0 @@
|
|||||||
import {CloudBackground} from "../component/fluid/cloud.tsx";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
import {createUseStyles} from "react-jss";
|
|
||||||
import {useWebsocket} from "../hook/websocket/u-ws.tsx";
|
|
||||||
import {Resp} from "../interface/response.ts";
|
|
||||||
|
|
||||||
const useClass = createUseStyles({
|
|
||||||
'@global': {
|
|
||||||
'@keyframes emerge': {
|
|
||||||
'0%': {
|
|
||||||
transform: 'scale(0) translate(-50%, -50%)',
|
|
||||||
opacity: 0
|
|
||||||
},
|
|
||||||
'80%': {
|
|
||||||
transform: 'scale(1.1) translate(-50%, -50%)',
|
|
||||||
opacity: 1
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
transform: 'scale(1) translate(-50%, -50%)',
|
|
||||||
opacity: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
margin: "0",
|
|
||||||
height: "100vh",
|
|
||||||
// background: "linear-gradient(45deg, #e6e9f0, #eef1f5)",
|
|
||||||
overflow: "hidden",
|
|
||||||
position: "relative",
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
width: '100%',
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: '#1661ab',
|
|
||||||
},
|
|
||||||
bubble: {
|
|
||||||
position: "absolute",
|
|
||||||
width: "100px",
|
|
||||||
height: "100px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontFamily: "'Microsoft Yahei', sans-serif",
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "rgba(255, 255, 255, 0.9)",
|
|
||||||
textShadow: "1px 1px 3px rgba(0,0,0,0.3)",
|
|
||||||
transition: "transform 0.3s ease",
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
animation: 'emerge 0.5s ease-out forwards,float 6s 0.5s ease-in-out infinite',
|
|
||||||
background: "radial-gradient(circle at 30% 30%,rgba(255, 255, 255, 0.8) 10%,rgba(255, 255, 255, 0.3) 50%,transparent 100%)",
|
|
||||||
border: "2px solid rgba(255, 255, 255, 0.5)",
|
|
||||||
boxShadow: "inset 0 -5px 15px rgba(255,255,255,0.3),0 5px 15px rgba(0,0,0,0.1)",
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
interface Bubble {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
color: string;
|
|
||||||
radius: number; // 新增半径属性
|
|
||||||
angle: number; // 新增角度属性
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Client {
|
|
||||||
client_type: 'desktop' | 'mobile' | 'tablet';
|
|
||||||
app_type: 'web';
|
|
||||||
room: string;
|
|
||||||
ip: number;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
register_at: string;
|
|
||||||
offer: RTCSessionDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Store {
|
|
||||||
client: Client | null
|
|
||||||
clients: Client[]
|
|
||||||
rtc: RTCPeerConnection | null
|
|
||||||
offer: RTCSessionDescription | null
|
|
||||||
candidate: RTCIceCandidate | null
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupDataChannel(ch: RTCDataChannel) {
|
|
||||||
ch.onopen = () => console.log('通道已打开!');
|
|
||||||
ch.onmessage = (e) => handleFileChunk(e.data);
|
|
||||||
ch.onclose = () => console.log('通道关闭');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 接收文件块
|
|
||||||
function handleFileChunk(chunk: any) {
|
|
||||||
console.log("[D] rtc file chunk =", chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LocalSharing: React.FC = () => {
|
|
||||||
const classes = useClass();
|
|
||||||
const [rtcStore, setRTCStore] = useState<Store>({} as Store)
|
|
||||||
const {connect, close} = useWebsocket({})
|
|
||||||
|
|
||||||
// 生成随机颜色
|
|
||||||
const generateColor = () => {
|
|
||||||
const hue = Math.random() * 360;
|
|
||||||
return `hsla(${hue},
|
|
||||||
${Math.random() * 30 + 40}%,
|
|
||||||
${Math.random() * 10 + 75}%, 0.9)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 防碰撞位置生成
|
|
||||||
const generateBubbles = (cs: Client[]) => {
|
|
||||||
if (!cs) return []
|
|
||||||
|
|
||||||
const BUBBLE_SIZE = 100;
|
|
||||||
const centerX = window.innerWidth / 2;
|
|
||||||
const centerY = window.innerHeight / 2;
|
|
||||||
|
|
||||||
const bubbles: Bubble[] = [];
|
|
||||||
let currentRadius = 0;
|
|
||||||
let angleStep = (2 * Math.PI) / 6; // 初始6个位置
|
|
||||||
|
|
||||||
for (let index = 0; index < cs.length; index++) {
|
|
||||||
let attempt = 0;
|
|
||||||
let validPosition = false;
|
|
||||||
|
|
||||||
if (cs[index].id == rtcStore.client?.id) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!validPosition && attempt < 100) {
|
|
||||||
// 螺旋布局算法
|
|
||||||
const angle = angleStep * (index + attempt);
|
|
||||||
const radius = currentRadius + (attempt * BUBBLE_SIZE * 0.8);
|
|
||||||
|
|
||||||
// 极坐标转笛卡尔坐标
|
|
||||||
const x = centerX + radius * Math.cos(angle);
|
|
||||||
const y = centerY + radius * Math.sin(angle);
|
|
||||||
|
|
||||||
// 边界检测
|
|
||||||
const inBounds = x >= 0 && x <= window.innerWidth - BUBBLE_SIZE &&
|
|
||||||
y >= 0 && y <= window.innerHeight - BUBBLE_SIZE;
|
|
||||||
|
|
||||||
// 碰撞检测
|
|
||||||
const collision = bubbles.some(pos => {
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(pos.x - x, 2) +
|
|
||||||
Math.pow(pos.y - y, 2)
|
|
||||||
);
|
|
||||||
return distance < BUBBLE_SIZE * 1.5;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (inBounds && !collision) {
|
|
||||||
bubbles.push({
|
|
||||||
id: cs[index].id,
|
|
||||||
name: cs[index].name,
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
color: generateColor(),
|
|
||||||
} as Bubble);
|
|
||||||
|
|
||||||
// 动态调整布局参数
|
|
||||||
currentRadius = Math.max(currentRadius, radius);
|
|
||||||
angleStep = (2 * Math.PI) / Math.max(6, bubbles.length * 0.7);
|
|
||||||
validPosition = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bubbles;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fn = async () => {
|
|
||||||
const rtc = new RTCPeerConnection({iceServers: [{urls: "stun:stun.qq.com:3478"}]})
|
|
||||||
const dataChannel = rtc.createDataChannel('fileTransfer', {ordered: true});
|
|
||||||
setupDataChannel(dataChannel);
|
|
||||||
const waitCandidate = new Promise<RTCIceCandidate | null>(resolve => {
|
|
||||||
rtc.onicecandidate = (e) => {
|
|
||||||
resolve(e.candidate)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const waitNegotiationneeded = new Promise<void>(resolve => {
|
|
||||||
rtc.onnegotiationneeded = async () => {
|
|
||||||
const _offer = await rtc.createOffer()
|
|
||||||
await rtc.setLocalDescription(_offer)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('[D] rtc step 1')
|
|
||||||
|
|
||||||
await waitNegotiationneeded
|
|
||||||
const candidate: RTCIceCandidate | null = await waitCandidate;
|
|
||||||
if (!candidate) throw new Error("candidate is null")
|
|
||||||
console.log('[D] rtc step 2')
|
|
||||||
|
|
||||||
const res = await fetch("/api/ulocal/register", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {"Content-Type": "application/json"},
|
|
||||||
body: JSON.stringify({candidate: candidate, offer: rtc.localDescription})
|
|
||||||
})
|
|
||||||
const jes = await res.json() as Resp<Client>;
|
|
||||||
setRTCStore(val => {
|
|
||||||
return {...val, client: jes.data, candidate: candidate, offer: rtc.localDescription}
|
|
||||||
})
|
|
||||||
|
|
||||||
const api = `${window.location.protocol === 'https' ? 'wss' : 'ws'}://${window.location.host}/api/ulocal/ws?id=${jes.data.id}`
|
|
||||||
console.log('[D] websocker url =', api)
|
|
||||||
connect(api, {})
|
|
||||||
|
|
||||||
const res2 = await fetch(`/api/ulocal/clients?room=${jes.data.room}`)
|
|
||||||
const jes2 = await res2.json() as Resp<Client[]>
|
|
||||||
setRTCStore(val => {
|
|
||||||
return {...val, clients: jes2.data}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn()
|
|
||||||
|
|
||||||
return () => close();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 气泡点击处理
|
|
||||||
const handleBubbleClick = async (id: string) => {
|
|
||||||
console.log('[D] click bubble!!!', id)
|
|
||||||
// await link(id)
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className={classes.container}>
|
|
||||||
<CloudBackground/>
|
|
||||||
<h1 className={classes.title}>{rtcStore.client?.name}</h1>
|
|
||||||
{rtcStore.clients && generateBubbles(rtcStore.clients).map(bubble => {
|
|
||||||
// const client = clients.find(c => c.id === bubble.id);
|
|
||||||
return rtcStore.client ? (
|
|
||||||
<div
|
|
||||||
key={bubble.id}
|
|
||||||
className={classes.bubble}
|
|
||||||
style={{
|
|
||||||
left: bubble.x,
|
|
||||||
top: bubble.y,
|
|
||||||
backgroundColor: bubble.color,
|
|
||||||
animationDelay:
|
|
||||||
`${Math.random() * 0.5}s,
|
|
||||||
${0.5 + Math.random() * 2}s`
|
|
||||||
}}
|
|
||||||
onClick={() => handleBubbleClick(bubble.id)}
|
|
||||||
>
|
|
||||||
{bubble.name}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
|
@ -10,11 +10,6 @@ export default defineConfig({
|
|||||||
target: 'http://127.0.0.1:9119',
|
target: 'http://127.0.0.1:9119',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/api/ulocal/ws': {
|
|
||||||
target: 'ws://127.0.0.1:9119',
|
|
||||||
rewriteWsOrigin: true,
|
|
||||||
ws: true,
|
|
||||||
},
|
|
||||||
'/ushare': {
|
'/ushare': {
|
||||||
target: 'http://127.0.0.1:9119',
|
target: 'http://127.0.0.1:9119',
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
|
2
go.mod
2
go.mod
@ -23,7 +23,6 @@ require (
|
|||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
|
github.com/jedib0t/go-pretty/v6 v6.6.7 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
@ -31,7 +30,6 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/mileusna/useragent v1.3.5 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -46,8 +46,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
|
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
|
||||||
@ -72,8 +70,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
|
|
||||||
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
|
|
||||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
@ -23,14 +23,6 @@ func Start(ctx context.Context) <-chan struct{} {
|
|||||||
app.Post("/api/ushare/:code", handler.ShareUpload()) // 分片上传接口
|
app.Post("/api/ushare/:code", handler.ShareUpload()) // 分片上传接口
|
||||||
app.Post("/api/uauth/login", handler.AuthLogin())
|
app.Post("/api/uauth/login", handler.AuthLogin())
|
||||||
|
|
||||||
{
|
|
||||||
api := app.Group("/api/ulocal")
|
|
||||||
api.Post("/register", handler.LocalRegister())
|
|
||||||
api.Post("/offer", handler.LocalOffer())
|
|
||||||
api.Get("/clients", handler.LocalClients())
|
|
||||||
api.Get("/ws", handler.LocalWS())
|
|
||||||
}
|
|
||||||
|
|
||||||
ready := make(chan struct{})
|
ready := make(chan struct{})
|
||||||
ln, err := net.Listen("tcp", opt.Cfg.Address)
|
ln, err := net.Listen("tcp", opt.Cfg.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,288 +0,0 @@
|
|||||||
// room controller:
|
|
||||||
// local share websocket room controller
|
|
||||||
// same remote IP as a
|
|
||||||
package controller
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/ushare/internal/pkg/tool"
|
|
||||||
"github.com/mileusna/useragent"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RoomClientType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ClientTypeDesktop RoomClientType = "desktop"
|
|
||||||
ClientTypeMobile RoomClientType = "mobile"
|
|
||||||
ClientTypeTablet RoomClientType = "tablet"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RoomAppType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RoomAppTypeWeb = "web"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RoomMessageType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
RoomMessageTypeEnter RoomMessageType = "enter"
|
|
||||||
RoomMessageTypeLeave RoomMessageType = "leave"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RoomOffer struct {
|
|
||||||
SDP string `json:"sdp"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoomCandidate struct {
|
|
||||||
Candidate string `json:"candidate"`
|
|
||||||
SdpMid string `json:"sdpMid"`
|
|
||||||
SdpMLineIndex int `json:"sdpMLineIndex"`
|
|
||||||
UsernameFragment string `json:"usernameFragment"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type roomClient struct {
|
|
||||||
sync.Mutex
|
|
||||||
controller *roomController
|
|
||||||
conn *websocket.Conn
|
|
||||||
ClientType RoomClientType `json:"client_type"`
|
|
||||||
AppType RoomAppType `json:"app_type"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
Room string `json:"room"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
RegisterAt time.Time `json:"register_at"`
|
|
||||||
Offer *RoomOffer `json:"offer"`
|
|
||||||
Candidate *RoomCandidate `json:"candidate"`
|
|
||||||
msgChan chan any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomClient) start(ctx context.Context) {
|
|
||||||
// start write
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
_ = rc.conn.Close()
|
|
||||||
return
|
|
||||||
case msg, _ := <-rc.msgChan:
|
|
||||||
err := rc.conn.WriteJSON(msg)
|
|
||||||
log.Debug("RoomClient: write json message, IP = %s, Id = %s, Name = %s, err = %v", rc.IP, rc.Id, rc.Name, err)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("RoomClient: write json message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// start read
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
mt, bs, err := rc.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("RoomClient: read message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
|
||||||
rc.controller.Unregister(rc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mt {
|
|
||||||
case websocket.PingMessage:
|
|
||||||
rs, _ := json.Marshal(map[string]any{"type": "pong", "time": time.Now().UnixMilli(), "Id": rc.Id, "Name": rc.Name})
|
|
||||||
if err := rc.conn.WriteMessage(websocket.PongMessage, rs); err != nil {
|
|
||||||
log.Error("RoomClient: response ping message failed, IP = %s, Id = %s, Name = %s, err = %s", rc.IP, rc.Id, rc.Name, err.Error())
|
|
||||||
}
|
|
||||||
case websocket.CloseMessage:
|
|
||||||
log.Debug("RoomClient: received close message, unregister IP = %s Id = %s, Name = %s", rc.IP, rc.Id, rc.Name)
|
|
||||||
rc.controller.Unregister(rc)
|
|
||||||
return
|
|
||||||
case websocket.TextMessage:
|
|
||||||
log.Debug("RoomClient: received text message, IP = %s, Id = %s, Name = %s, text = %s", rc.IP, rc.Id, rc.Name, string(bs))
|
|
||||||
case websocket.BinaryMessage:
|
|
||||||
log.Debug("RoomClient: received bytes message, IP = %s, Id = %s, Name = %s, text = %s", rc.IP, rc.Id, rc.Name, string(bs))
|
|
||||||
// todo
|
|
||||||
//msg := new(model.Message)
|
|
||||||
//if err = json.Unmarshal(bs, msg); err != nil {
|
|
||||||
// log.Error("RoomClient: unmarshal message failed, id = %s, name = %s, err = %s", rc.Id, rc.Name, err.Error())
|
|
||||||
// continue
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//switch msg.Type {
|
|
||||||
//case model.WSMessageTypeOffer:
|
|
||||||
// rc.Lock()
|
|
||||||
// rc.Offer = msg.Body
|
|
||||||
// rc.Unlock()
|
|
||||||
//case model.WSMessageTypeCandidate:
|
|
||||||
// rc.Lock()
|
|
||||||
// rc.Candidate = msg.Body
|
|
||||||
// rc.Unlock()
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
type roomController struct {
|
|
||||||
sync.Mutex
|
|
||||||
ctx context.Context
|
|
||||||
rooms map[string]map[string]*roomClient // map[room_id(remote-IP)][Id]
|
|
||||||
notReadies map[string]*roomClient
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
RoomController = &roomController{
|
|
||||||
rooms: make(map[string]map[string]*roomClient),
|
|
||||||
notReadies: make(map[string]*roomClient),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (rc *roomController) Start(ctx context.Context) {
|
|
||||||
rc.ctx = ctx
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(1 * time.Minute)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-rc.ctx.Done():
|
|
||||||
return
|
|
||||||
case now := <-ticker.C:
|
|
||||||
for _, nrc := range rc.notReadies {
|
|
||||||
if now.Sub(nrc.RegisterAt).Minutes() > 1 {
|
|
||||||
rc.Lock()
|
|
||||||
delete(rc.notReadies, nrc.Id)
|
|
||||||
rc.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) Register(ip, userAgent string, candidate *RoomCandidate, offer *RoomOffer) *roomClient {
|
|
||||||
nrc := &roomClient{
|
|
||||||
controller: rc,
|
|
||||||
ClientType: ClientTypeDesktop,
|
|
||||||
AppType: RoomAppTypeWeb,
|
|
||||||
IP: ip,
|
|
||||||
Id: uuid.Must(uuid.NewV7()).String(),
|
|
||||||
Name: tool.RandomName(),
|
|
||||||
msgChan: make(chan any, 1),
|
|
||||||
RegisterAt: time.Now(),
|
|
||||||
Candidate: candidate,
|
|
||||||
Offer: offer,
|
|
||||||
}
|
|
||||||
|
|
||||||
ua := useragent.Parse(userAgent)
|
|
||||||
switch {
|
|
||||||
case ua.Mobile:
|
|
||||||
nrc.ClientType = ClientTypeMobile
|
|
||||||
case ua.Tablet:
|
|
||||||
nrc.ClientType = ClientTypeTablet
|
|
||||||
}
|
|
||||||
|
|
||||||
key := "local"
|
|
||||||
if !tool.IsPrivateIP(ip) {
|
|
||||||
key = ip
|
|
||||||
}
|
|
||||||
|
|
||||||
nrc.Room = key
|
|
||||||
|
|
||||||
rc.Lock()
|
|
||||||
|
|
||||||
log.Debug("controller.room: registry client, IP = %s(%s), Id = %s, Name = %s", key, nrc.IP, nrc.Id, nrc.Name)
|
|
||||||
rc.notReadies[nrc.Id] = nrc
|
|
||||||
if _, ok := rc.rooms[nrc.Room]; !ok {
|
|
||||||
rc.rooms[nrc.Room] = make(map[string]*roomClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.Unlock()
|
|
||||||
|
|
||||||
return nrc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) Enter(conn *websocket.Conn, id string) {
|
|
||||||
client, ok := rc.notReadies[id]
|
|
||||||
if !ok {
|
|
||||||
log.Warn("controller.room: entry room id not exist, id = %s", id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.Lock()
|
|
||||||
|
|
||||||
if _, ok = rc.rooms[client.Room]; !ok {
|
|
||||||
log.Warn("controller.room: entry room not exist, room = %s, id = %s, name = %s", client.Room, id, client.Name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.rooms[client.Room][id] = client
|
|
||||||
client.conn = conn
|
|
||||||
|
|
||||||
rc.Unlock()
|
|
||||||
|
|
||||||
client.start(rc.ctx)
|
|
||||||
|
|
||||||
rc.Broadcast(client.Room, map[string]any{"type": RoomMessageTypeEnter, "time": time.Now().UnixMilli(), "body": client})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) List(room string) []*roomClient {
|
|
||||||
clientList := make([]*roomClient, 0)
|
|
||||||
|
|
||||||
rc.Lock()
|
|
||||||
defer rc.Unlock()
|
|
||||||
|
|
||||||
clients, ok := rc.rooms[room]
|
|
||||||
if !ok {
|
|
||||||
return clientList
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, client := range clients {
|
|
||||||
clientList = append(clientList, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientList
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) Broadcast(room string, msg any) {
|
|
||||||
for _, client := range rc.rooms[room] {
|
|
||||||
select {
|
|
||||||
case client.msgChan <- msg:
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
log.Warn("RoomController: broadcast timeout, room = %s, client Id = %s, IP = %s", room, client.Id, client.IP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) Unregister(client *roomClient) {
|
|
||||||
key := "local"
|
|
||||||
if !tool.IsPrivateIP(client.IP) {
|
|
||||||
key = client.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("controller.room: unregister client, IP = %s(%s), Id = %s, Name = %s", client.IP, key, client.Id, client.Name)
|
|
||||||
|
|
||||||
rc.Lock()
|
|
||||||
delete(rc.rooms[key], client.Id)
|
|
||||||
rc.Unlock()
|
|
||||||
|
|
||||||
rc.Broadcast(key, map[string]any{"type": RoomMessageTypeLeave, "time": time.Now().UnixMilli(), "body": client})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rc *roomController) Offer(room, id string, offer *RoomOffer) {
|
|
||||||
if _, ok := rc.rooms[room]; !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := rc.rooms[room][id]; !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rc.rooms[room][id].msgChan <- map[string]any{
|
|
||||||
"type": "offer",
|
|
||||||
"offer": offer,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/loveuer/nf"
|
|
||||||
"github.com/loveuer/nf/nft/log"
|
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"github.com/loveuer/ushare/internal/controller"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LocalRegister() nf.HandlerFunc {
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Candidate *controller.RoomCandidate `json:"candidate"`
|
|
||||||
Offer *controller.RoomOffer `json:"offer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
ip = c.IP(true)
|
|
||||||
ua = c.Get("User-Agent")
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(map[string]interface{}{"msg": err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
client := controller.RoomController.Register(ip, ua, req.Candidate, req.Offer)
|
|
||||||
|
|
||||||
return resp.Resp200(c, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LocalClients() nf.HandlerFunc {
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
room := c.Query("room")
|
|
||||||
if room == "" {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": "room can't be empty"})
|
|
||||||
}
|
|
||||||
|
|
||||||
list := controller.RoomController.List(room)
|
|
||||||
|
|
||||||
return resp.Resp200(c, list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LocalWS() nf.HandlerFunc {
|
|
||||||
upgrader := websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
|
|
||||||
id := c.Query("id")
|
|
||||||
|
|
||||||
if id == "" {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"error": "id is empty"})
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("LocalWS: failed to upgrade websocket connection, err = %s", err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.RoomController.Enter(conn, id)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LocalOffer() nf.HandlerFunc {
|
|
||||||
return func(c *nf.Ctx) error {
|
|
||||||
type Req struct {
|
|
||||||
Room string `json:"room"`
|
|
||||||
Id string `json:"id"`
|
|
||||||
Offer *controller.RoomOffer `json:"offer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
req = new(Req)
|
|
||||||
)
|
|
||||||
|
|
||||||
if err = c.BodyParser(req); err != nil {
|
|
||||||
return c.Status(http.StatusBadRequest).JSON(map[string]string{"err": err.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.RoomController.Offer(req.Room, req.Id, req.Offer)
|
|
||||||
|
|
||||||
return resp.Resp200(c, req.Offer)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type WSMessageType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
WSMessageTypeOffer WSMessageType = "offer"
|
|
||||||
WSMessageTypeCandidate WSMessageType = "candidate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
Type WSMessageType `json:"type"`
|
|
||||||
Time int64 `json:"time"`
|
|
||||||
Body any `json:"body"`
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package tool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
privateIPv4Blocks []*net.IPNet
|
|
||||||
privateIPv6Blocks []*net.IPNet
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// IPv4私有地址段
|
|
||||||
for _, cidr := range []string{
|
|
||||||
"10.0.0.0/8", // A类私有地址
|
|
||||||
"172.16.0.0/12", // B类私有地址
|
|
||||||
"192.168.0.0/16", // C类私有地址
|
|
||||||
"169.254.0.0/16", // 链路本地地址
|
|
||||||
"127.0.0.0/8", // 环回地址
|
|
||||||
} {
|
|
||||||
_, block, _ := net.ParseCIDR(cidr)
|
|
||||||
privateIPv4Blocks = append(privateIPv4Blocks, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPv6私有地址段
|
|
||||||
for _, cidr := range []string{
|
|
||||||
"fc00::/7", // 唯一本地地址
|
|
||||||
"fe80::/10", // 链路本地地址
|
|
||||||
"::1/128", // 环回地址
|
|
||||||
} {
|
|
||||||
_, block, _ := net.ParseCIDR(cidr)
|
|
||||||
privateIPv6Blocks = append(privateIPv6Blocks, block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsPrivateIP(ipStr string) bool {
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理IPv4和IPv4映射的IPv6地址
|
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
|
||||||
for _, block := range privateIPv4Blocks {
|
|
||||||
if block.Contains(ip4) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理IPv6地址
|
|
||||||
for _, block := range privateIPv6Blocks {
|
|
||||||
if block.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@ -3,31 +3,14 @@ package tool
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"math/big"
|
"math/big"
|
||||||
mrand "math/rand"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
letters = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
letterNum = []byte("0123456789")
|
letterNum = []byte("0123456789")
|
||||||
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
letterLow = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||||
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
letterCap = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
letterSyb = []byte("!@#$%^&*()_+-=")
|
letterSyb = []byte("!@#$%^&*()_+-=")
|
||||||
adjectives = []string{
|
|
||||||
"开心的", "灿烂的", "温暖的", "阳光的", "活泼的",
|
|
||||||
"聪明的", "优雅的", "幸运的", "甜蜜的", "勇敢的",
|
|
||||||
"宁静的", "热情的", "温柔的", "幽默的", "坚强的",
|
|
||||||
"迷人的", "神奇的", "快乐的", "健康的", "自由的",
|
|
||||||
"梦幻的", "勤劳的", "真诚的", "浪漫的", "自信的",
|
|
||||||
}
|
|
||||||
|
|
||||||
plants = []string{
|
|
||||||
"苹果", "香蕉", "橘子", "葡萄", "草莓",
|
|
||||||
"西瓜", "樱桃", "菠萝", "柠檬", "蜜桃",
|
|
||||||
"蓝莓", "芒果", "石榴", "甜瓜", "雪梨",
|
|
||||||
"番茄", "南瓜", "土豆", "青椒", "洋葱",
|
|
||||||
"黄瓜", "萝卜", "豌豆", "玉米", "蘑菇",
|
|
||||||
"菠菜", "茄子", "芹菜", "莲藕", "西兰花",
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RandomInt(max int64) int64 {
|
func RandomInt(max int64) int64 {
|
||||||
@ -69,7 +52,3 @@ func RandomPassword(length int, withSymbol bool) string {
|
|||||||
}
|
}
|
||||||
return string(result)
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandomName() string {
|
|
||||||
return adjectives[mrand.Intn(len(adjectives))] + plants[mrand.Intn(len(plants))]
|
|
||||||
}
|
|
||||||
|
1
main.go
1
main.go
@ -33,7 +33,6 @@ func main() {
|
|||||||
opt.Init(ctx)
|
opt.Init(ctx)
|
||||||
controller.UserManager.Start(ctx)
|
controller.UserManager.Start(ctx)
|
||||||
controller.MetaManager.Start(ctx)
|
controller.MetaManager.Start(ctx)
|
||||||
controller.RoomController.Start(ctx)
|
|
||||||
api.Start(ctx)
|
api.Start(ctx)
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
149
page/bubble.html
149
page/bubble.html
@ -1,149 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>柔和气泡</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(45deg, #e6e9f0, #eef1f5);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
position: absolute;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: 'Microsoft Yahei', sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
text-shadow: 1px 1px 3px rgba(0,0,0,0.3);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
animation: float 6s ease-in-out infinite;
|
|
||||||
/* 泡泡细节 */
|
|
||||||
background: radial-gradient(circle at 30% 30%,
|
|
||||||
rgba(255, 255, 255, 0.8) 10%,
|
|
||||||
rgba(255, 255, 255, 0.3) 50%,
|
|
||||||
transparent 100%);
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
|
||||||
box-shadow: inset 0 -5px 15px rgba(255,255,255,0.3),
|
|
||||||
0 5px 15px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 高光效果 */
|
|
||||||
.bubble::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
border-radius: 50%;
|
|
||||||
top: 15px;
|
|
||||||
right: 15px;
|
|
||||||
filter: blur(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble:hover {
|
|
||||||
transform: scale(1.05) rotate(3deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
|
||||||
33% { transform: translateY(-20px) rotate(3deg); }
|
|
||||||
66% { transform: translateY(10px) rotate(-3deg); }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
const prefixes = ['宁静', '温暖', '甜蜜', '柔和', '灿烂', '神秘', '清爽'];
|
|
||||||
const suffixes = ['梦境', '时光', '旋律', '花园', '云端', '海洋', '晨露'];
|
|
||||||
const existingPositions = [];
|
|
||||||
const BUBBLE_SIZE = 100;
|
|
||||||
|
|
||||||
// 检测碰撞
|
|
||||||
function checkCollision(x, y) {
|
|
||||||
return existingPositions.some(pos => {
|
|
||||||
const distance = Math.sqrt(
|
|
||||||
Math.pow(x - pos.x, 2) +
|
|
||||||
Math.pow(y - pos.y, 2)
|
|
||||||
);
|
|
||||||
return distance < BUBBLE_SIZE * 1.2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBubble() {
|
|
||||||
let x, y;
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
// 生成非重叠位置
|
|
||||||
do {
|
|
||||||
x = Math.random() * (window.innerWidth - BUBBLE_SIZE);
|
|
||||||
y = Math.random() * (window.innerHeight - BUBBLE_SIZE);
|
|
||||||
attempts++;
|
|
||||||
} while (checkCollision(x, y) && attempts < 100);
|
|
||||||
|
|
||||||
if (attempts >= 100) return;
|
|
||||||
|
|
||||||
existingPositions.push({x, y});
|
|
||||||
|
|
||||||
const bubble = document.createElement('div');
|
|
||||||
bubble.className = 'bubble';
|
|
||||||
|
|
||||||
// 随机颜色
|
|
||||||
const hue = Math.random() * 360;
|
|
||||||
bubble.style.backgroundColor = `hsla(${hue},
|
|
||||||
${Math.random() * 30 + 40}%,
|
|
||||||
${Math.random() * 10 + 75}%, 0.9)`;
|
|
||||||
|
|
||||||
// 随机名称
|
|
||||||
bubble.textContent = `${prefixes[Math.random()*prefixes.length|0]}的${suffixes[Math.random()*suffixes.length|0]}`;
|
|
||||||
|
|
||||||
// 位置和动画
|
|
||||||
bubble.style.left = x + 'px';
|
|
||||||
bubble.style.top = y + 'px';
|
|
||||||
bubble.style.animationDelay = Math.random() * 2 + 's';
|
|
||||||
|
|
||||||
// 点击效果
|
|
||||||
bubble.addEventListener('click', () => {
|
|
||||||
bubble.style.transition = 'transform 0.5s, opacity 0.5s';
|
|
||||||
bubble.style.transform = 'scale(1.5)';
|
|
||||||
bubble.style.opacity = '0';
|
|
||||||
setTimeout(() => {
|
|
||||||
bubble.remove();
|
|
||||||
existingPositions.splice(existingPositions.findIndex(
|
|
||||||
pos => pos.x === x && pos.y === y), 1);
|
|
||||||
createBubble();
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(bubble);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化15个气泡
|
|
||||||
for (let i = 0; i < 15; i++) createBubble();
|
|
||||||
|
|
||||||
// 自动补充气泡
|
|
||||||
setInterval(() => {
|
|
||||||
if (document.querySelectorAll('.bubble').length < 20) {
|
|
||||||
createBubble();
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// 窗口大小变化时重置
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
existingPositions.length = 0;
|
|
||||||
document.querySelectorAll('.bubble').forEach(bubble => bubble.remove());
|
|
||||||
for (let i = 0; i < 15; i++) createBubble();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Reference in New Issue
Block a user