This repository was archived by the owner on Mar 13, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBingLocalNetChatClient.py
More file actions
445 lines (351 loc) · 15.8 KB
/
BingLocalNetChatClient.py
File metadata and controls
445 lines (351 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# 冰氏局域网去中心化聊天系统 - 客户端
# 作者注: 只是原则上的去中心化, 实际上一旦掌握密钥就可以通过客户端的一些后门进行控制
# 库导入
from PySide2.QtCore import QEvent, Qt
from PySide2.QtWidgets import QApplication, QMessageBox, QInputDialog, QLineEdit, QMainWindow, QWidget
from PySide2.QtUiTools import QUiLoader
from PySide2.QtGui import QIcon, QFontDatabase, QPixmap
from threading import Thread
from win10toast_click import ToastNotifier
from PIL import ImageGrab
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import socket, time, sys, configparser, os, uuid, wget, requests, yaml
localHost = ''
# INI 配置文件
class Config:
def __init__(self, file):
self.file = file
if not os.path.exists(self.file):
cf = configparser.ConfigParser()
# 不存在配置文件时创建配置文件
cf.add_section("common")
cf.set("common", "chat_host", '255.255.255.255')
cf.set("common", "chat_port", '30930')
cf.set('common', "file_port", "62059")
cf.set('common', 'broadcast_port', '31129')
cf.write(open(self.file, 'w', encoding='utf-8'))
self.config = configparser.ConfigParser()
self.config.read(self.file)
def writeConfig(self, section, option, value):
self.config.set(section, option, value)
self.config.write(open(self.file, "w", encoding="utf-8"))
self.config.read(self.file)
# YAML 配置文件
class YamlConfig:
def __init__(self, file):
self.file = file
if not os.path.exists(self.file):
data = {'username': ''}
with open(self.file, 'w', encoding='utf-8') as f:
yaml.dump(data, f)
with open(self.file, 'r', encoding="utf-8") as f:
self.config = f.read()
def getConfig(self):
return yaml.load(self.config, Loader=yaml.FullLoader)
def writeConfig(self, key, value):
data = self.getConfig()
data[key] = value
with open(self.file, 'w', encoding='utf-8') as f:
yaml.dump(data, f)
with open(self.file, 'r', encoding="utf-8") as f:
self.config = f.read()
# 下载文件
def downloadFile(url, path):
wget.download(url, path)
r = requests.get(url, stream=True)
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=1024): # 1024 bytes
if chunk:
f.write(chunk)
# 关于我们
class About(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load('style/About.ui')
self.ui.setWindowTitle("关于 冰氏局域网去中心化聊天系统")
self.ui.label.setPixmap(QPixmap('style/25385f0f6af17a35.jpg'))
self.ui.textBrowser.anchorClicked.connect(self.clickURL)
# 点击链接
def clickURL(self, url):
os.startfile(url.toString())
# 主窗口
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = QUiLoader().load('style/MainWindow.ui')
# Alpha
self.ui.setWindowTitle("冰氏局域网去中心化聊天系统 - 客户端 - v1.1")
self.ui.actionSetUserName.triggered.connect(self.setUserName)
# self.ui.actionChannelConnect.triggered.connect(self.changeChannel)
self.ui.actionExit.triggered.connect(self.exitClient)
self.ui.actionAbout.triggered.connect(self.openAbout)
self.ui.actionClearChat.triggered.connect(self.clearChat)
self.ui.textBrowser.anchorClicked.connect(self.clickURL)
self.ui.sendText.installEventFilter(self)
# 发送按钮
self.ui.sendButton.clicked.connect(self.sendMsg)
self.ui.screenshotButton.clicked.connect(self.sendScreenshot)
# 如果没有设置用户名, 询问用户名
if yamlConfig.getConfig()['username'] == '':
while True:
title, okPressed = QInputDialog.getText(self, "填写昵称", "设置你的昵称为:", QLineEdit.Normal, "")
if okPressed and title != "":
# config.writeConfig("common", "username", title)
yamlConfig.writeConfig('username', title)
break
# 发送信息
def sendMsg(self):
try:
# 如果为空则不发送
if self.ui.sendText.toPlainText() == "": return
data = {'username': yamlConfig.getConfig()['username'], 'type': 'msg',
'text': self.ui.sendText.toPlainText()}
sender.sendMsg(data)
self.ui.sendText.clear()
# 提交一份自己的
self.ui.textBrowser.append('')
self.ui.textBrowser.append('<b><font color="#4CAF50">{}</font></b>'.format(
'[{}] 你:'.format(time.strftime("%H:%M:%S", time.localtime()))))
for line in data['text'].split('\n'):
self.ui.textBrowser.append('<font color="#4CAF50">{}</font>'.format(line))
self.ui.textBrowser.ensureCursorVisible()
except:
QMessageBox.critical(self.ui, '错误', '发送信息错误')
# 发送屏幕截图
def sendScreenshot(self):
im = ImageGrab.grab()
fileID = uuid.uuid1()
fileName = str(fileID)
im.save('cache/{}.jpg'.format(fileID), 'jpeg')
try:
data = {'username': yamlConfig.getConfig()['username'], 'type': 'img',
'port': config.config['common']['file_port'], 'file': '{}.jpg'.format(fileName)}
sender.sendMsg(data)
self.ui.textBrowser.append('')
self.ui.textBrowser.append('<b><font color="#4CAF50">{}</font></b>'.format(
'[{}] 你:'.format(time.strftime("%H:%M:%S", time.localtime()))))
self.ui.textBrowser.append(
'<a href="file:///{path}\cache\{file}"><img src="cache/{file}" width=500></a>'.format(
file=str(fileName) + '.jpg', path=os.getcwd()))
self.ui.textBrowser.ensureCursorVisible()
except:
QMessageBox.critical(self.ui, '错误', '发送屏幕截图错误')
# 回车按键
def eventFilter(self, widget, event):
if self.ui.returnSend.isChecked():
# 判断事件类型是否是键盘事件
if event.type() == QEvent.KeyPress:
# 判断是否是回车
if event.key() == Qt.Key_Return:
self.sendMsg()
return QWidget.eventFilter(self, widget, event)
# 设置昵称
def setUserName(self):
title, okPressed = QInputDialog.getText(self, "填写昵称", "设置你的昵称为:", QLineEdit.Normal,
yamlConfig.getConfig()['username'])
if okPressed and title == "":
QMessageBox.critical(self.ui, '警告', '昵称不可为空')
elif okPressed:
yamlConfig.writeConfig('username', title)
QMessageBox.information(self.ui, '设置成功', '你已成功将昵称修改为 {}'.format(title))
# 切换频道
def changeChannel(self):
global nowChatChannel, listener, sender, th_listener
title, okPressed = QInputDialog.getInt(self, "更换频道", "输入相应频道的端口:", QLineEdit.Normal, 0)
if okPressed and 0 <= title <= 65535:
nowChatChannel = title
# 停止服务并重新启动
listener = th_listener = None
listener = Listener(nowChatChannel)
th_listener = Thread(target=listener.run)
th_listener.start()
sender.changePort(nowChatChannel)
# 输出提示
main.ui.textBrowser.append('')
main.ui.textBrowser.append(
'<b><font color="#F44336">{}</font></b>'.format('[提示] 你已成功将端口修改为 {}...'.format(nowChatChannel)))
main.ui.textBrowser.ensureCursorVisible()
elif okPressed:
QMessageBox.critical(self.ui, '警告', '频道端口超出范围 (0 - 65535)')
# 退出程序
def exitClient(self):
choice = QMessageBox.question(self.ui, '确认退出', '退出后不保留历史记录')
if choice == QMessageBox.Yes: os._exit(0)
# 清除聊天
def clearChat(self):
self.ui.textBrowser.clear()
statusBar.addStatus('已成功清除聊天记录')
# 打开关于
def openAbout(self):
about.ui.show()
# 点击链接
def clickURL(self, url):
os.startfile(url.toString())
# 发送者 Sender
class Sender:
# 初始化时需要 username
def __init__(self, network, port):
self.network = network
self.port = port
self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 加入信息
data = {'username': yamlConfig.getConfig()['username'], 'type': "join"}
self.sendMsg(data)
# 发送数据
def sendMsg(self, data):
self.s.sendto(str(data).encode("utf-8"), (self.network, self.port))
def close(self):
self.s.close()
def changePort(self, port):
# 离开信息
data = {'username': yamlConfig.getConfig()['username'], 'type': "quit"}
self.sendMsg(data)
# 更换端口
self.port = port
# 加入信息
data = {'username': yamlConfig.getConfig()['username'], 'type': "join"}
self.sendMsg(data)
# 接收者 Listener
class Listener:
def __init__(self, port):
self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
time.sleep(0.1)
self.s.bind(('', port))
# 获取到数据时的操作
def run(self):
lastData = {}
while True:
rawData, address = self.s.recvfrom(65535)
data = eval(rawData)
print('[DEBUG] Server received from {}:{}'.format(address, rawData.decode('utf-8')))
# 如果是本地发送的, 则忽略
if address[0] == localHost:
# time.sleep(0.2)
continue
# 临时解决重复接收
if lastData == data:
continue
else:
lastData = data
# 显示
if data['type'] == 'msg':
main.ui.textBrowser.append('')
main.ui.textBrowser.append('<b><font color="#000000">[{}] {}({}):</font></b>'.format(
time.strftime("%H:%M:%S", time.localtime()), data['username'], address[0]))
for line in data['text'].split('\n'):
main.ui.textBrowser.append('<font color="#000000">{}</font>'.format(line))
# Win10 推送
self.sendWindowsMessage("你收到了来自 {}({}) 的信息".format(data['username'], address[0]), data['text'])
elif data['type'] == 'img':
# 下载
# wget.download('http://{}:{}/{}'.format(address[0], data['port'], data['file']), 'cache/{}'.format(data['file']))
downloadFile('http://{}:{}/{}'.format(address[0], data['port'], data['file']),
'cache/{}'.format(data['file']))
# 输出文本
main.ui.textBrowser.append('')
main.ui.textBrowser.append('<b><font color="#000000">[{}] {}({}):</font></b>'.format(
time.strftime("%H:%M:%S", time.localtime()), data['username'], address[0]))
main.ui.textBrowser.append(
'<a href="file:///{path}\cache\{file}"><img src="cache/{file}" width=500></a>'.format(
file=data['file'], path=os.getcwd()))
# Win10 推送
self.sendWindowsMessage("你收到了来自 {}({}) 的信息".format(data['username'], address[0]), '[屏幕截图]')
elif data['type'] == 'join':
main.ui.textBrowser.append('')
main.ui.textBrowser.append('<b><font color="#3F51B5">{}</font></b>'.format(
'[系统] 用户 {}({}) 进入了此频道'.format(data['username'], address[0])))
elif data['type'] == 'quit':
main.ui.textBrowser.append('')
main.ui.textBrowser.append('<b><font color="#3F51B5">{}</font></b>'.format(
'[系统] 用户 {}({}) 离开了此频道'.format(data['username'], address[0])))
main.ui.textBrowser.ensureCursorVisible()
# Windows 10 消息
def sendWindowsMessage(self, title, message):
try:
toaster.show_toast(title, message, icon_path="logo.ico", threaded=True, duration=None)
except:
print("[警告] 发送 Windows 消息出错")
# win10 通知出错时提示的窗口
class MsgNotice(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load('style/MsgNotice.ui')
self.ui.setWindowTitle("关于 冰氏局域网去中心化聊天系统")
# 文件传输服务
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory='cache', **kwargs)
class fileHttp:
def __init__(self):
self.server_obj = None
if not os.path.exists('cache'):
os.makedirs('cache')
def run(self):
self.server_obj = ThreadingHTTPServer(('', config.config['common'].getint('file_port')), Handler)
self.server_obj.serve_forever()
# 底部提示框
class StatusBar:
def __init__(self):
self.statusbar = main.ui.statusbar
self.status = []
self.th_statusbar = Thread(target=self.run)
self.th_statusbar.start()
def run(self):
while True:
if len(self.status) > 0:
self.statusbar.showMessage(self.status.pop(0))
time.sleep(7)
else:
self.statusbar.showMessage(
"你当前使用昵称 {} 位于 {} 频道".format(yamlConfig.getConfig()['username'], nowChatChannel))
time.sleep(0.5)
def addStatus(self, status):
self.status.append(status)
# TODO 管理员:软件更新、禁言、强制关闭软件
# Windows 通知
toaster = ToastNotifier()
# 主函数
if __name__ == '__main__':
# 获取本机地址
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
localHost = s.getsockname()[0]
s.close()
# 读取配置
config = Config('config.ini')
yamlConfig = YamlConfig('config.yml')
# 准备文件和图片的传输
file_http = fileHttp()
th_fileHttp = Thread(target=file_http.run)
th_fileHttp.start()
# 基础加载
app = QApplication(sys.argv)
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Black.ttf")
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Bold.ttf")
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Light.ttf")
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Medium.ttf")
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Regular.ttf")
QFontDatabase.addApplicationFont("font/HarmonyOS_Sans_SC_Thin.ttf")
app.setWindowIcon(QIcon('logo.png'))
main = MainWindow()
main.ui.show()
about = About()
# 将聊天频道传给变量, 以便调用
nowChatChannel = config.config['common'].getint('chat_port')
# 启动状态管理器
statusBar = StatusBar()
# 提示连接到默认频道
main.ui.textBrowser.append('')
main.ui.textBrowser.append('<b><font color="#F44336">{}</font></b>'.format('[提示] 你已成功连接到默认频道...'))
main.ui.textBrowser.ensureCursorVisible()
# 监听聊天频道
listener = Listener(nowChatChannel)
th_listener = Thread(target=listener.run)
th_listener.start()
# 发送端
sender = Sender(config.config['common']['chat_host'], nowChatChannel)
# 退出准备
app.exec_()
os._exit(0)