Merge branch 'chat' of https://github.com/SaptakS/onionshare into SaptakS-chat

This commit is contained in:
Micah Lee 2020-08-19 18:56:55 -04:00
commit 8e1b34ce13
16 changed files with 823 additions and 36 deletions

View File

@ -87,6 +87,9 @@ def main(cwd=None):
parser.add_argument(
"--website", action="store_true", dest="website", help="Publish website"
)
parser.add_argument(
"--chat", action="store_true", dest="chat", help="Start chat server"
)
# Tor connection-related args
parser.add_argument(
"--local-only",
@ -196,6 +199,7 @@ def main(cwd=None):
receive = bool(args.receive)
website = bool(args.website)
chat = bool(args.chat)
local_only = bool(args.local_only)
connect_timeout = int(args.connect_timeout)
config_filename = args.config
@ -214,6 +218,8 @@ def main(cwd=None):
mode = "receive"
elif website:
mode = "website"
elif chat:
mode = "chat"
else:
mode = "share"

View File

@ -221,6 +221,16 @@ class Common:
r = random.SystemRandom()
return "-".join(r.choice(wordlist) for _ in range(word_count))
def build_username(self, word_count=2):
"""
Returns a random string made of words from the wordlist, such as "deter-trig".
"""
with open(self.get_resource_path("wordlist.txt")) as f:
wordlist = f.read().split()
r = random.SystemRandom()
return "-".join(r.choice(wordlist) for _ in range(word_count))
@staticmethod
def random_string(num_bytes, output_len=None):
"""

View File

@ -49,6 +49,7 @@ class ModeSettings:
"share": {"autostop_sharing": True, "filenames": []},
"receive": {"data_dir": self.build_default_receive_data_dir()},
"website": {"disable_csp": False, "filenames": []},
"chat": {"room": "default"},
}
self._settings = {}

141
onionshare/web/chat_mode.py Normal file
View File

@ -0,0 +1,141 @@
from flask import (
Request,
request,
render_template,
make_response,
jsonify,
redirect,
session,
)
from werkzeug.utils import secure_filename
from flask_socketio import emit, join_room, leave_room
class ChatModeWeb:
"""
All of the web logic for chat mode
"""
def __init__(self, common, web):
self.common = common
self.common.log("ChatModeWeb", "__init__")
self.web = web
# This tracks users in the room
self.connected_users = []
# This tracks the history id
self.cur_history_id = 0
self.define_routes()
def define_routes(self):
"""
The web app routes for chatting
"""
@self.web.app.route("/")
def index():
history_id = self.cur_history_id
self.cur_history_id += 1
session["name"] = (
session.get("name")
if session.get("name")
else self.common.build_username()
)
session["room"] = self.web.settings.default_settings["chat"]["room"]
self.web.add_request(
request.path, {"id": history_id, "status_code": 200},
)
self.web.add_request(self.web.REQUEST_LOAD, request.path)
r = make_response(
render_template(
"chat.html",
static_url_path=self.web.static_url_path,
username=session.get("name"),
)
)
return self.web.add_security_headers(r)
@self.web.app.route("/update-session-username", methods=["POST"])
def update_session_username():
history_id = self.cur_history_id
data = request.get_json()
session["name"] = data.get("username", session.get("name"))
self.web.add_request(
request.path, {"id": history_id, "status_code": 200},
)
self.web.add_request(self.web.REQUEST_LOAD, request.path)
r = make_response(
jsonify(
username=session.get("name"),
success=True,
)
)
return self.web.add_security_headers(r)
@self.web.socketio.on("joined", namespace="/chat")
def joined(message):
"""Sent by clients when they enter a room.
A status message is broadcast to all people in the room."""
self.connected_users.append(session.get("name"))
join_room(session.get("room"))
emit(
"status",
{
"msg": "{} has joined.".format(session.get("name")),
"connected_users": self.connected_users,
"user": session.get("name"),
},
room=session.get("room"),
)
@self.web.socketio.on("text", namespace="/chat")
def text(message):
"""Sent by a client when the user entered a new message.
The message is sent to all people in the room."""
emit(
"message",
{"msg": "{}: {}".format(session.get("name"), message["msg"])},
room=session.get("room"),
)
@self.web.socketio.on("update_username", namespace="/chat")
def update_username(message):
"""Sent by a client when the user updates their username.
The message is sent to all people in the room."""
current_name = session.get("name")
session["name"] = message["username"]
self.connected_users[
self.connected_users.index(current_name)
] = session.get("name")
emit(
"status",
{
"msg": "{} has updated their username to: {}".format(
current_name, session.get("name")
),
"connected_users": self.connected_users,
"old_name": current_name,
"new_name": session.get("name"),
},
room=session.get("room"),
)
@self.web.socketio.on("disconnect", namespace="/chat")
def disconnect():
"""Sent by clients when they disconnect from a room.
A status message is broadcast to all people in the room."""
self.connected_users.remove(session.get("name"))
leave_room(session.get("room"))
emit(
"status",
{
"msg": "{} has left the room.".format(session.get("name")),
"connected_users": self.connected_users,
},
room=session.get("room"),
)

View File

@ -20,12 +20,14 @@ from flask import (
__version__ as flask_version,
)
from flask_httpauth import HTTPBasicAuth
from flask_socketio import SocketIO
from .. import strings
from .share_mode import ShareModeWeb
from .receive_mode import ReceiveModeWeb, ReceiveModeWSGIMiddleware, ReceiveModeRequest
from .website_mode import WebsiteModeWeb
from .chat_mode import ChatModeWeb
# Stub out flask's show_server_banner function, to avoiding showing warnings that
# are not applicable to OnionShare
@ -134,12 +136,17 @@ class Web:
self.share_mode = None
self.receive_mode = None
self.website_mode = None
self.chat_mode = None
if self.mode == "share":
self.share_mode = ShareModeWeb(self.common, self)
elif self.mode == "receive":
self.receive_mode = ReceiveModeWeb(self.common, self)
elif self.mode == "website":
self.website_mode = WebsiteModeWeb(self.common, self)
elif self.mode == "chat":
self.socketio = SocketIO()
self.socketio.init_app(self.app)
self.chat_mode = ChatModeWeb(self.common, self)
def get_mode(self):
if self.mode == "share":
@ -148,6 +155,8 @@ class Web:
return self.receive_mode
elif self.mode == "website":
return self.website_mode
elif self.mode == "chat":
return self.chat_mode
else:
return None
@ -366,7 +375,10 @@ class Web:
host = "127.0.0.1"
self.running = True
self.app.run(host=host, port=port, threaded=True)
if self.mode == "chat":
self.socketio.run(self.app, host=host, port=port)
else:
self.app.run(host=host, port=port, threaded=True)
def stop(self, port):
"""

View File

@ -31,6 +31,7 @@ class GuiCommon:
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
MODE_CHAT = "chat"
def __init__(self, common, qtapp, local_only):
self.common = common

View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import os
import random
import string
from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
from onionshare.onion import *
from onionshare.common import Common
from onionshare.web import Web
from .. import Mode
from ....widgets import MinimumWidthWidget
class ChatMode(Mode):
"""
Parts of the main window UI for sharing files.
"""
success = QtCore.pyqtSignal()
error = QtCore.pyqtSignal(str)
def init(self):
"""
Custom initialization for ChatMode.
"""
# Create the Web object
self.web = Web(self.common, True, self.settings, "chat")
# Header
self.header_label.setText(strings._("gui_new_tab_chat_button"))
# Server status
self.server_status.set_mode("chat")
self.server_status.server_started_finished.connect(self.update_primary_action)
self.server_status.server_stopped.connect(self.update_primary_action)
self.server_status.server_canceled.connect(self.update_primary_action)
# Tell server_status about web, then update
self.server_status.web = self.web
self.server_status.update()
# Top bar
top_bar_layout = QtWidgets.QHBoxLayout()
top_bar_layout.addStretch()
# Main layout
self.main_layout = QtWidgets.QVBoxLayout()
self.main_layout.addLayout(top_bar_layout)
self.main_layout.addWidget(self.primary_action)
self.main_layout.addStretch()
self.main_layout.addWidget(MinimumWidthWidget(700))
# Column layout
self.column_layout = QtWidgets.QHBoxLayout()
self.column_layout.addLayout(self.main_layout)
# Wrapper layout
self.wrapper_layout = QtWidgets.QVBoxLayout()
self.wrapper_layout.addWidget(self.header_label)
self.wrapper_layout.addLayout(self.column_layout)
self.setLayout(self.wrapper_layout)
def get_stop_server_autostop_timer_text(self):
"""
Return the string to put on the stop server button, if there's an auto-stop timer
"""
return strings._("gui_share_stop_server_autostop_timer")
def autostop_timer_finished_should_stop_server(self):
"""
The auto-stop timer expired, should we stop the server? Returns a bool
"""
self.server_status.stop_server()
self.server_status_label.setText(strings._("close_on_autostop_timer"))
return True
def start_server_custom(self):
"""
Starting the server.
"""
# Reset web counters
self.web.chat_mode.cur_history_id = 0
self.web.reset_invalid_passwords()
def start_server_step2_custom(self):
"""
Step 2 in starting the server. Zipping up files.
"""
# Continue
self.starting_server_step3.emit()
self.start_server_finished.emit()
def cancel_server_custom(self):
"""
Log that the server has been cancelled
"""
self.common.log("ChatMode", "cancel_server")
def handle_tor_broke_custom(self):
"""
Connection to Tor broke.
"""
self.primary_action.hide()
def on_reload_settings(self):
"""
We should be ok to re-enable the 'Start Receive Mode' button now.
"""
self.primary_action.show()
def update_primary_action(self):
self.common.log("ChatMode", "update_primary_action")

View File

@ -270,6 +270,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setText(strings._("gui_share_start_server"))
elif self.mode == self.common.gui.MODE_WEBSITE:
self.server_button.setText(strings._("gui_share_start_server"))
elif self.mode == self.common.gui.MODE_CHAT:
self.server_button.setText(strings._("gui_chat_start_server"))
else:
self.server_button.setText(strings._("gui_receive_start_server"))
self.server_button.setToolTip("")
@ -282,6 +284,8 @@ class ServerStatus(QtWidgets.QWidget):
self.server_button.setText(strings._("gui_share_stop_server"))
elif self.mode == self.common.gui.MODE_WEBSITE:
self.server_button.setText(strings._("gui_share_stop_server"))
elif self.mode == self.common.gui.MODE_CHAT:
self.server_button.setText(strings._("gui_chat_stop_server"))
else:
self.server_button.setText(strings._("gui_receive_stop_server"))
elif self.status == self.STATUS_WORKING:

View File

@ -28,6 +28,7 @@ from onionshare.mode_settings import ModeSettings
from .mode.share_mode import ShareMode
from .mode.receive_mode import ReceiveMode
from .mode.website_mode import WebsiteMode
from .mode.chat_mode import ChatMode
from .server_status import ServerStatus
@ -93,6 +94,16 @@ class Tab(QtWidgets.QWidget):
)
website_description.setWordWrap(True)
self.chat_button = QtWidgets.QPushButton(
strings._("gui_new_tab_chat_button")
)
self.chat_button.setStyleSheet(self.common.gui.css["mode_new_tab_button"])
self.chat_button.clicked.connect(self.chat_mode_clicked)
chat_description = QtWidgets.QLabel(
strings._("gui_new_tab_chat_description")
)
chat_description.setWordWrap(True)
new_tab_layout = QtWidgets.QVBoxLayout()
new_tab_layout.addStretch(1)
new_tab_layout.addWidget(self.share_button)
@ -103,6 +114,9 @@ class Tab(QtWidgets.QWidget):
new_tab_layout.addSpacing(50)
new_tab_layout.addWidget(self.website_button)
new_tab_layout.addWidget(website_description)
new_tab_layout.addSpacing(50)
new_tab_layout.addWidget(self.chat_button)
new_tab_layout.addWidget(chat_description)
new_tab_layout.addStretch(3)
new_tab_inner = QtWidgets.QWidget()
@ -278,6 +292,43 @@ class Tab(QtWidgets.QWidget):
self.update_server_status_indicator()
self.timer.start(500)
def chat_mode_clicked(self):
self.common.log("Tab", "chat_mode_clicked")
self.mode = self.common.gui.MODE_CHAT
self.new_tab.hide()
self.chat_mode = ChatMode(self)
self.chat_mode.change_persistent.connect(self.change_persistent)
self.layout.addWidget(self.chat_mode)
self.chat_mode.show()
self.chat_mode.init()
self.chat_mode.server_status.server_started.connect(
self.update_server_status_indicator
)
self.chat_mode.server_status.server_stopped.connect(
self.update_server_status_indicator
)
self.chat_mode.start_server_finished.connect(
self.update_server_status_indicator
)
self.chat_mode.stop_server_finished.connect(
self.update_server_status_indicator
)
self.chat_mode.stop_server_finished.connect(self.stop_server_finished)
self.chat_mode.start_server_finished.connect(self.clear_message)
self.chat_mode.server_status.button_clicked.connect(self.clear_message)
self.chat_mode.server_status.url_copied.connect(self.copy_url)
self.chat_mode.server_status.hidservauth_copied.connect(
self.copy_hidservauth
)
self.change_title.emit(self.tab_id, strings._("gui_new_tab_chat_button"))
self.update_server_status_indicator()
self.timer.start(500)
def update_server_status_indicator(self):
# Set the status image
if self.mode == self.common.gui.MODE_SHARE:
@ -486,6 +537,8 @@ class Tab(QtWidgets.QWidget):
return self.share_mode
elif self.mode == self.common.gui.MODE_RECEIVE:
return self.receive_mode
elif self.mode == self.common.gui.MODE_CHAT:
return self.chat_mode
else:
return self.website_mode
else:

219
poetry.lock generated
View File

@ -62,13 +62,33 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "dev"
description = "Python 2.7 backport of the \"dis\" module from Python 3.5+"
marker = "sys_platform == \"darwin\""
name = "dis3"
category = "main"
description = "DNS toolkit"
name = "dnspython"
optional = false
python-versions = ">=3.6"
version = "2.0.0"
[package.extras]
curio = ["curio (>=1.2)", "sniffio (>=1.1)"]
dnssec = ["cryptography (>=2.6)"]
doh = ["requests", "requests-toolbelt"]
idna = ["idna (>=2.1)"]
trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"]
[[package]]
category = "main"
description = "Highly concurrent networking library"
name = "eventlet"
optional = false
python-versions = "*"
version = "0.1.3"
version = "0.25.2"
[package.dependencies]
dnspython = ">=1.15.0"
greenlet = ">=0.3"
monotonic = ">=1.4"
six = ">=1.10.0"
[[package]]
category = "main"
@ -100,6 +120,18 @@ version = "4.1.0"
[package.dependencies]
Flask = "*"
[[package]]
category = "main"
description = "Socket.IO integration for Flask applications"
name = "flask-socketio"
optional = false
python-versions = "*"
version = "4.3.1"
[package.dependencies]
Flask = ">=0.9"
python-socketio = ">=4.3.0"
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
@ -108,6 +140,14 @@ optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]]
category = "main"
description = "Lightweight in-process concurrent programming"
name = "greenlet"
optional = false
python-versions = "*"
version = "0.4.16"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
@ -132,6 +172,14 @@ zipp = ">=0.5"
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
[[package]]
category = "dev"
description = "iniconfig: brain-dead simple config-ini parsing"
name = "iniconfig"
optional = false
python-versions = "*"
version = "1.0.1"
[[package]]
category = "main"
description = "Various helpers to pass data to untrusted environments and back."
@ -173,6 +221,14 @@ optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]]
category = "main"
description = "An implementation of time.monotonic() for Python 2 & < 3.3"
name = "monotonic"
optional = false
python-versions = "*"
version = "1.5"
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
@ -242,14 +298,28 @@ description = "PyInstaller bundles a Python application and all its dependencies
marker = "sys_platform == \"darwin\""
name = "pyinstaller"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "3.6"
python-versions = "*"
version = "4.0"
[package.dependencies]
altgraph = "*"
dis3 = "*"
macholib = ">=1.8"
pyinstaller-hooks-contrib = ">=2020.6"
setuptools = "*"
[package.extras]
encryption = ["tinyaes (>=1.0.0)"]
hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"]
[[package]]
category = "dev"
description = "Community maintained hooks for PyInstaller"
marker = "sys_platform == \"darwin\""
name = "pyinstaller-hooks-contrib"
optional = false
python-versions = "*"
version = "2020.7"
[[package]]
category = "dev"
description = "Python parsing module"
@ -291,24 +361,25 @@ description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.3"
version = "6.0.1"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
iniconfig = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
py = ">=1.8.2"
toml = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
checkqa-mypy = ["mypy (v0.761)"]
checkqa_mypy = ["mypy (0.780)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
@ -337,6 +408,37 @@ pytest = ">=3.0.0"
dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
category = "main"
description = "Engine.IO server"
name = "python-engineio"
optional = false
python-versions = "*"
version = "3.13.2"
[package.dependencies]
six = ">=1.9.0"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
category = "main"
description = "Socket.IO server"
name = "python-socketio"
optional = false
python-versions = "*"
version = "4.6.0"
[package.dependencies]
python-engineio = ">=3.13.0"
six = ">=1.9.0"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)", "websockets (>=7.0)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
category = "main"
description = "QR Code image generator"
@ -389,27 +491,27 @@ optional = false
python-versions = "*"
version = "1.8.0"
[[package]]
category = "dev"
description = "Python Library for Tom's Obvious, Minimal Language"
name = "toml"
optional = false
python-versions = "*"
version = "0.10.1"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.9"
version = "1.25.10"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
category = "dev"
description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.2.5"
[[package]]
category = "main"
description = "The comprehensive WSGI web application library."
@ -436,7 +538,8 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "71c32a60a36f2e66745f800f5cab96e8d2551f5959acc8b07aa9003d6f3f702b"
content-hash = "de18641607a5f3bf11a3051b84eb8a02d4263f435f3554c1aa5860136011cbf3"
lock-version = "1.0"
python-versions = "^3.7"
[metadata.files]
@ -468,10 +571,13 @@ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
dis3 = [
{file = "dis3-0.1.3-py2-none-any.whl", hash = "sha256:61f7720dd0d8749d23fda3d7227ce74d73da11c2fade993a67ab2f9852451b14"},
{file = "dis3-0.1.3-py3-none-any.whl", hash = "sha256:30b6412d33d738663e8ded781b138f4b01116437f0872aa56aa3adba6aeff218"},
{file = "dis3-0.1.3.tar.gz", hash = "sha256:9259b881fc1df02ed12ac25f82d4a85b44241854330b1a651e40e0c675cb2d1e"},
dnspython = [
{file = "dnspython-2.0.0-py3-none-any.whl", hash = "sha256:40bb3c24b9d4ec12500f0124288a65df232a3aa749bb0c39734b782873a2544d"},
{file = "dnspython-2.0.0.zip", hash = "sha256:044af09374469c3a39eeea1a146e8cac27daec951f1f1f157b1962fc7cb9d1b7"},
]
eventlet = [
{file = "eventlet-0.25.2-py2.py3-none-any.whl", hash = "sha256:955f2cf538829bfcb7b3aa885ace40e8ae5965dcd5b876c384d0c5869702db1d"},
{file = "eventlet-0.25.2.tar.gz", hash = "sha256:4c8ab42c51bff55204fef43cff32616558bedbc7538d876bb6a96ce820c7f9ed"},
]
flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
@ -481,9 +587,32 @@ flask-httpauth = [
{file = "Flask-HTTPAuth-4.1.0.tar.gz", hash = "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"},
{file = "Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl", hash = "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7"},
]
flask-socketio = [
{file = "Flask-SocketIO-4.3.1.tar.gz", hash = "sha256:36c1d5765010d1f4e4f05b4cc9c20c289d9dc70698c88d1addd0afcfedc5b062"},
{file = "Flask_SocketIO-4.3.1-py2.py3-none-any.whl", hash = "sha256:3668675bf7763c5b5f56689d439f07356e89c0a52e0c9e9cd3cc08563c07b252"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
greenlet = [
{file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"},
{file = "greenlet-0.4.16-cp27-cp27m-win32.whl", hash = "sha256:df7de669cbf21de4b04a3ffc9920bc8426cab4c61365fa84d79bf97401a8bef7"},
{file = "greenlet-0.4.16-cp27-cp27m-win_amd64.whl", hash = "sha256:1429dc183b36ec972055e13250d96e174491559433eb3061691b446899b87384"},
{file = "greenlet-0.4.16-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5ea034d040e6ab1d2ae04ab05a3f37dbd719c4dee3804b13903d4cc794b1336e"},
{file = "greenlet-0.4.16-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c196a5394c56352e21cb7224739c6dd0075b69dd56f758505951d1d8d68cf8a9"},
{file = "greenlet-0.4.16-cp35-cp35m-win32.whl", hash = "sha256:1000038ba0ea9032948e2156a9c15f5686f36945e8f9906e6b8db49f358e7b52"},
{file = "greenlet-0.4.16-cp35-cp35m-win_amd64.whl", hash = "sha256:1b805231bfb7b2900a16638c3c8b45c694334c811f84463e52451e00c9412691"},
{file = "greenlet-0.4.16-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e5db19d4a7d41bbeb3dd89b49fc1bc7e6e515b51bbf32589c618655a0ebe0bf0"},
{file = "greenlet-0.4.16-cp36-cp36m-win32.whl", hash = "sha256:eac2a3f659d5f41d6bbfb6a97733bc7800ea5e906dc873732e00cebb98cec9e4"},
{file = "greenlet-0.4.16-cp36-cp36m-win_amd64.whl", hash = "sha256:7eed31f4efc8356e200568ba05ad645525f1fbd8674f1e5be61a493e715e3873"},
{file = "greenlet-0.4.16-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:682328aa576ec393c1872615bcb877cf32d800d4a2f150e1a5dc7e56644010b1"},
{file = "greenlet-0.4.16-cp37-cp37m-win32.whl", hash = "sha256:3a35e33902b2e6079949feed7a2dafa5ac6f019da97bd255842bb22de3c11bf5"},
{file = "greenlet-0.4.16-cp37-cp37m-win_amd64.whl", hash = "sha256:b0b2a984bbfc543d144d88caad6cc7ff4a71be77102014bd617bd88cfb038727"},
{file = "greenlet-0.4.16-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d83c1d38658b0f81c282b41238092ed89d8f93c6e342224ab73fb39e16848721"},
{file = "greenlet-0.4.16-cp38-cp38-win32.whl", hash = "sha256:e695ac8c3efe124d998230b219eb51afb6ef10524a50b3c45109c4b77a8a3a92"},
{file = "greenlet-0.4.16-cp38-cp38-win_amd64.whl", hash = "sha256:133ba06bad4e5f2f8bf6a0ac434e0fd686df749a86b3478903b92ec3a9c0c90b"},
{file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
@ -492,6 +621,10 @@ importlib-metadata = [
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
]
iniconfig = [
{file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"},
{file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"},
]
itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
@ -539,6 +672,10 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
]
monotonic = [
{file = "monotonic-1.5-py2.py3-none-any.whl", hash = "sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7"},
{file = "monotonic-1.5.tar.gz", hash = "sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0"},
]
more-itertools = [
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
@ -591,7 +728,11 @@ pycryptodome = [
{file = "pycryptodome-3.9.8.tar.gz", hash = "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4"},
]
pyinstaller = [
{file = "PyInstaller-3.6.tar.gz", hash = "sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"},
{file = "pyinstaller-4.0.tar.gz", hash = "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0"},
]
pyinstaller-hooks-contrib = [
{file = "pyinstaller-hooks-contrib-2020.7.tar.gz", hash = "sha256:74936d044f319cd7a9dca322b46a818fcb6e2af1c67af62e8a6a3121eb2863d2"},
{file = "pyinstaller_hooks_contrib-2020.7-py2.py3-none-any.whl", hash = "sha256:5b6e06ba6072499189f5b8e1623d5f0414962941aac370ee4f842de25455be5b"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
@ -629,8 +770,8 @@ pysocks = [
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
]
pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
{file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"},
{file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"},
]
pytest-faulthandler = [
{file = "pytest-faulthandler-2.0.1.tar.gz", hash = "sha256:ed72bbce87ac344da81eb7d882196a457d4a1026a3da4a57154dacd85cd71ae5"},
@ -640,6 +781,14 @@ pytest-qt = [
{file = "pytest-qt-3.3.0.tar.gz", hash = "sha256:714b0bf86c5313413f2d300ac613515db3a1aef595051ab8ba2ffe619dbe8925"},
{file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"},
]
python-engineio = [
{file = "python-engineio-3.13.2.tar.gz", hash = "sha256:36b33c6aa702d9b6a7f527eec6387a2da1a9a24484ec2f086d76576413cef04b"},
{file = "python_engineio-3.13.2-py2.py3-none-any.whl", hash = "sha256:cfded18156862f94544a9f8ef37f56727df731c8552d7023f5afee8369be2db6"},
]
python-socketio = [
{file = "python-socketio-4.6.0.tar.gz", hash = "sha256:358d8fbbc029c4538ea25bcaa283e47f375be0017fcba829de8a3a731c9df25a"},
{file = "python_socketio-4.6.0-py2.py3-none-any.whl", hash = "sha256:d437f797c44b6efba2f201867cf02b8c96b97dff26d4e4281ac08b45817cd522"},
]
qrcode = [
{file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"},
{file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
@ -655,13 +804,13 @@ six = [
stem = [
{file = "stem-1.8.0.tar.gz", hash = "sha256:a0b48ea6224e95f22aa34c0bc3415f0eb4667ddeae3dfb5e32a6920c185568c2"},
]
urllib3 = [
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
toml = [
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
urllib3 = [
{file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"},
{file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"},
]
werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},

View File

@ -28,6 +28,8 @@ requests = "*"
stem = "*"
urllib3 = "*"
Werkzeug = "*"
flask-socketio = "^4.3.0"
eventlet = "^0.25.2"
qrcode = "^6.1"
[tool.poetry.dev-dependencies]

View File

@ -17,6 +17,9 @@
"gui_share_start_server": "Start sharing",
"gui_share_stop_server": "Stop sharing",
"gui_share_stop_server_autostop_timer": "Stop Sharing ({})",
"gui_chat_stop_server_autostop_timer": "Stop Chat Server ({})",
"gui_chat_start_server": "Start chat server",
"gui_chat_stop_server": "Stop chat server",
"gui_stop_server_autostop_timer_tooltip": "Auto-stop timer ends at {}",
"gui_start_server_autostart_timer_tooltip": "Auto-start timer ends at {}",
"gui_receive_start_server": "Start Receive Mode",
@ -187,6 +190,8 @@
"gui_new_tab_receive_description": "Turn your computer into an online dropbox. People will be able to use Tor Browser to send files to your computer.",
"gui_new_tab_website_button": "Publish Website",
"gui_new_tab_website_description": "Host a static HTML onion website from your computer.",
"gui_new_tab_chat_button": "Start Chat Server",
"gui_new_tab_chat_description": "Start an onion chat server and use it to chat in Tor Browser.",
"gui_close_tab_warning_title": "Are you sure?",
"gui_close_tab_warning_persistent_description": "This tab is persistent. If you close it you'll lose the onion address that it's using. Are you sure you want to close it?",
"gui_close_tab_warning_share_description": "You're in the process of sending files. Are you sure you want to close this tab?",

View File

@ -167,6 +167,67 @@ ul.breadcrumbs li a:link, ul.breadcrumbs li a:visited {
}
}
.chat-container {
display: flex;
}
.chat-users {
width: 20%;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 2px;
overflow: auto;
background: #f2f2f2;
}
.chat-users .editable-username {
display: flex;
padding: 1rem;
flex-direction: column;
}
.chat-users #user-list li {
margin-bottom: 1em;
}
.chat-wrapper {
display: flex;
flex-direction: column;
flex: 1;
margin: 0 1rem;
height: calc(100vh - (45px + 2em));
}
.chat-wrapper #chat {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 2px;
flex: 1;
overflow: auto;
background: #f2f2f2;
padding: 0 1rem;
}
.chat-wrapper .chat-form {
display: flex;
}
.chat-wrapper input#new-message {
height: 100%;
}
@media (max-width: 992px) {
.chat-users .editable-username {
display: block;
}
.chat-users input#username {
width: 90%;
}
}
.no-js {
display: none;
}
.upload-wrapper {
align-items: center;
justify-content: center;

160
share/static/js/chat.js Normal file
View File

@ -0,0 +1,160 @@
$(function(){
$(document).ready(function(){
$('.chat-container').removeClass('no-js');
var socket = io.connect('http://' + document.domain + ':' + location.port + '/chat');
// Store current username received from app context
var current_username = $('#username').val();
// On browser connect, emit a socket event to be added to
// room and assigned random username
socket.on('connect', function() {
socket.emit('joined', {});
});
// Triggered on any status change by any user, such as some
// user joined, or changed username, or left, etc.
socket.on('status', function(data) {
addMessageToRoom(data, current_username, 'status');
});
// Triggered when message is received from a user. Even when sent
// by self, it get triggered after the server sends back the emit.
socket.on('message', function(data) {
addMessageToRoom(data, current_username, 'chat');
});
// Triggered when disconnected either by server stop or timeout
socket.on('disconnect', function(data) {
addMessageToRoom({'msg': 'The chat server is disconnected.'}, current_username, 'status');
})
socket.on('connect_error', function(error) {
console.log("error");
})
// Trigger new message on enter or click of send message button.
$('#new-message').on('keypress', function(e) {
var code = e.keyCode || e.which;
if (code == 13) {
emitMessage(socket);
}
});
$('#send-button').on('click', function(e) {
emitMessage(socket);
});
// Keep buttons disabled unless changed or not empty
$('#username').on('keyup',function(event) {
if ($('#username').val() !== '' && $('#username').val() !== current_username) {
$('#update-username').removeAttr('disabled');
if (event.keyCode == 13) {
current_username = updateUsername(socket);
}
} else {
$('#update-username').attr('disabled', true);
}
});
// Update username
$('#update-username').on('click', function() {
current_username = updateUsername(socket);
});
// Show warning of losing data
$(window).on('beforeunload', function (e) {
e.preventDefault();
e.returnValue = '';
return '';
});
});
});
var addMessageToRoom = function(data, current_username, messageType) {
var scrollDiff = getScrollDiffBefore();
if (messageType === 'status') {
addStatusMessage(data.msg);
if (data.connected_users) {
addUserList(data.connected_users, current_username);
}
} else if (messageType === 'chat') {
addChatMessage(data.msg)
}
scrollBottomMaybe(scrollDiff);
}
var emitMessage = function(socket) {
var text = $('#new-message').val();
$('#new-message').val('');
$('#chat').scrollTop($('#chat')[0].scrollHeight);
socket.emit('text', {msg: text});
}
var updateUsername = function(socket) {
var username = $('#username').val();
socket.emit('update_username', {username: username});
$.ajax({
method: 'POST',
url: `http://${document.domain}:${location.port}/update-session-username`,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({'username': username})
}).done(function(response) {
console.log(response);
});
$('#update-username').attr('disabled', true);
return username;
}
/************************************/
/********* Util Functions ***********/
/************************************/
var createUserListHTML = function(connected_users, current_user) {
var userListHTML = '';
connected_users.sort();
connected_users.forEach(function(username) {
if (username !== current_user) {
userListHTML += `<li>${sanitizeHTML(username)}</li>`;
}
});
return userListHTML;
}
var getScrollDiffBefore = function() {
return $('#chat').scrollTop() - ($('#chat')[0].scrollHeight - $('#chat')[0].offsetHeight);
}
var scrollBottomMaybe = function(scrollDiff) {
// Scrolls to bottom if the user is scrolled at bottom
// if the user has scrolled upp, it wont scroll at bottom.
// Note: when a user themselves send a message, it will still
// scroll to the bottom even if they had scrolled up before.
if (scrollDiff > 0) {
$('#chat').scrollTop($('#chat')[0].scrollHeight);
}
}
var addStatusMessage = function(message) {
$('#chat').append(
`<p><small><i>${sanitizeHTML(message)}</i></small></p>`
);
}
var addChatMessage = function(message) {
$('#chat').append(`<p>${sanitizeHTML(message)}</p>`);
}
var addUserList = function(connected_users, current_username) {
$('#user-list').html(
createUserListHTML(
connected_users,
current_username
)
);
}
var sanitizeHTML = function(str) {
var temp = document.createElement('span');
temp.textContent = str;
return temp.innerHTML;
};

3
share/static/js/socket.io.min.js vendored Normal file

File diff suppressed because one or more lines are too long

46
share/templates/chat.html Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>OnionShare</title>
<link href="{{ static_url_path }}/img/favicon.ico" rel="icon" type="image/x-icon">
<link rel="stylesheet" rel="subresource" type="text/css" href="{{ static_url_path }}/css/style.css" media="all">
</head>
<body>
<header class="clearfix">
<img class="logo" src="{{ static_url_path }}/img/logo.png" title="OnionShare">
<h1>OnionShare</h1>
</header>
<noscript>
<p>
Chat <b>requires JavaScript</b>, so you must set your Tor Browser security
level to <b>Safer</b> or <b>Standard</b> to join.
</p>
</noscript>
<div class="chat-container no-js">
<div class="chat-users">
<div class="editable-username">
<input id="username" value="{{ username }}" />
<button id="update-username" disabled>Save</button>
</div>
<ul id="user-list">
</ul>
</div>
<div class="chat-wrapper">
<p class="chat-header">Chat Messages</p>
<div id="chat"></div>
<div class="chat-form">
<p><input type="text" id="new-message" name="new-message" placeholder="Type your message"/></p>
<p><button type="button" id="send-button" class="button">Send Message</button></p>
</div>
</div>
</div>
<script src="{{ static_url_path }}/js/jquery-3.5.1.min.js"></script>
<script src="{{ static_url_path }}/js/socket.io.min.js"></script>
<script async src="{{ static_url_path }}/js/chat.js"></script>
</body>
</html>