from flask import Flask, render_template_string, request, send_from_directory, session from flask_socketio import SocketIO, emit, disconnect from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime import time import os import base64 import uuid import subprocess app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'secret!') app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:////sdcard/Documents/chat.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False UPLOAD_FOLDER = '/sdcard/Documents/uploads' os.makedirs(UPLOAD_FOLDER, exist_ok=True) db = SQLAlchemy(app) socketio = SocketIO(app, cors_allowed_origins="*") def export_chat_history(): query = "SELECT id, username, recipient, text, file_type, status, timestamp, is_edited, reply_to_id, is_deleted FROM message;" db_path = '/sdcard/Documents/chat.db' out_path = '/sdcard/Documents/chat_history.txt' if os.path.exists(db_path): try: with open(out_path, 'w') as f: subprocess.run(['sqlite3', '-header', '-column', db_path, query], stdout=f, check=True) except Exception as e: print(f"Error exporting history: {e}") class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) is_admin = db.Column(db.Boolean, default=False) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Message(db.Model): id = db.Column(db.String(50), primary_key=True) username = db.Column(db.String(100), nullable=False) recipient = db.Column(db.String(100), nullable=True) text = db.Column(db.Text, nullable=True) media_data = db.Column(db.Text, nullable=True) file_type = db.Column(db.String(20), default='text') timestamp = db.Column(db.String(20), nullable=False) status = db.Column(db.String(20), default='sent') is_private = db.Column(db.Boolean, default=False) is_edited = db.Column(db.Boolean, default=False) is_deleted = db.Column(db.Boolean, default=False) reply_to_id = db.Column(db.String(50), nullable=True) def to_dict(self): mdata = self.media_data or '' ftype = self.file_type or 'text' if mdata: if mdata.endswith(('.jpg', '.jpeg', '.png')) or 'image/' in mdata: ftype = 'image' elif mdata.endswith(('.webm', '.mp3')) or 'audio/' in mdata: ftype = 'audio' elif mdata.endswith('.mp4') or 'video/' in mdata: ftype = 'video' reactions = Reaction.query.filter_by(message_id=self.id).all() rx_dict = {} for r in reactions: if r.emoji not in rx_dict: rx_dict[r.emoji] = [] rx_dict[r.emoji].append(r.username) reply_info = None if self.reply_to_id: parent = db.session.get(Message, self.reply_to_id) if parent: reply_info = { 'id': parent.id, 'username': parent.username, 'text': 'This message was deleted' if parent.is_deleted else (parent.text or '[Media]') } user_prof = UserProfile.query.filter_by(username=self.username).first() avatar_color = user_prof.avatar_url if (user_prof and user_prof.avatar_url) else '#3b82f6' return { 'id': self.id, 'username': self.username, 'recipient': self.recipient, 'text': self.text or '', 'media_data': mdata, 'file_type': ftype, 'timestamp': self.timestamp, 'status': self.status, 'is_private': self.is_private, 'is_edited': self.is_edited, 'is_deleted': self.is_deleted, 'reply_to': reply_info, 'reactions': rx_dict, 'avatar_color': avatar_color } class CallLog(db.Model): id = db.Column(db.String(50), primary_key=True) caller = db.Column(db.String(100), nullable=False) receiver = db.Column(db.String(100), nullable=False) call_type = db.Column(db.String(20), default='outgoing') duration = db.Column(db.String(20), default='00:00') timestamp = db.Column(db.String(20), nullable=False) def to_dict(self): return { 'id': self.id, 'caller': self.caller, 'receiver': self.receiver, 'call_type': self.call_type, 'duration': self.duration, 'timestamp': self.timestamp } class Reaction(db.Model): id = db.Column(db.Integer, primary_key=True) message_id = db.Column(db.String(50), db.ForeignKey('message.id'), nullable=False) username = db.Column(db.String(100), nullable=False) emoji = db.Column(db.String(10), nullable=False) class UserProfile(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(100), unique=True, nullable=False) bio = db.Column(db.String(255), default="Hey there! I am using Termux Chat.") avatar_url = db.Column(db.Text, default="#3b82f6") last_seen = db.Column(db.String(50), default="Online") def to_dict(self): return { 'username': self.username, 'bio': self.bio, 'avatar_url': self.avatar_url, 'last_seen': self.last_seen } with app.app_context(): db.create_all() online_users = {} def save_media_file(base64_data, file_type): try: if ',' in base64_data: header, encoded = base64_data.split(',', 1) else: encoded = base64_data data_bytes = base64.b64decode(encoded) ext = 'jpg' if file_type == 'image' else ('mp4' if file_type == 'video' else 'webm') filename = f"{uuid.uuid4().hex}.{ext}" filepath = os.path.join(UPLOAD_FOLDER, filename) with open(filepath, 'wb') as f: f.write(data_bytes) return f"/uploads/{filename}" except Exception as e: print(f"Error saving media file: {e}") return "" @app.route('/uploads/') def uploaded_file(filename): return send_from_directory(UPLOAD_FOLDER, filename) HTML_TEMPLATE = """ Termux Live Chat & Calls
Active Users:
Attachment Preview: preview
Replying to...
00:00
Termux Encrypted Voice Call 00:00
U

Calling...

Connecting securely...

""" @app.route('/') def index(): return render_template_string(HTML_TEMPLATE) @app.route('/api/auth', methods=['POST']) def auth_api(): data = request.json or {} action = data.get('action') uname = data.get('username') pword = data.get('password') if not uname or not pword: return {'error': 'Missing credentials'}, 400 user = User.query.filter_by(username=uname).first() if action == 'register': if user: return {'error': 'Username taken'}, 400 is_first = User.query.count() == 0 new_user = User(username=uname, is_admin=is_first) new_user.set_password(pword) db.session.add(new_user) db.session.commit() session['username'] = uname session['is_admin'] = is_first return {'username': uname, 'is_admin': is_first} elif action == 'login': if not user or not user.check_password(pword): return {'error': 'Invalid credentials'}, 400 session['username'] = uname session['is_admin'] = user.is_admin return {'username': uname, 'is_admin': user.is_admin} return {'error': 'Invalid action'}, 400 @socketio.on('connect') def handle_connect(): history = Message.query.filter_by(is_private=False).all() emit('load_global_history', [msg.to_dict() for msg in history]) @socketio.on('disconnect') def handle_disconnect(): if request.sid in online_users: user_info = online_users.pop(request.sid) uname = user_info['username'] if isinstance(user_info, dict) else user_info prof = UserProfile.query.filter_by(username=uname).first() if prof: prof.last_seen = datetime.now().strftime("%I:%M %p") db.session.commit() emit('system_message', f"{uname} left the chat", broadcast=True) emit('update_users', online_users, broadcast=True) @socketio.on('user_joined') def handle_user_joined(data): uname = data.get('username') if isinstance(data, dict) else data bio = data.get('bio', '') if isinstance(data, dict) else '' color = data.get('avatar_color', '#3b82f6') if isinstance(data, dict) else '#3b82f6' online_users[request.sid] = {'username': uname, 'bio': bio, 'avatar_color': color} prof = UserProfile.query.filter_by(username=uname).first() if not prof: prof = UserProfile(username=uname, bio=bio, avatar_url=color, last_seen="Online") db.session.add(prof) else: prof.last_seen = "Online" prof.avatar_url = color db.session.commit() emit('system_message', f"{uname} joined the chat", broadcast=True) emit('update_users', online_users, broadcast=True) @socketio.on('update_profile') def handle_update_profile(data): old_uname = data.get('old_username') new_uname = data.get('username') bio = data.get('bio') color = data.get('avatar_color', '#3b82f6') prof = UserProfile.query.filter_by(username=old_uname).first() if not prof and old_uname != new_uname: prof = UserProfile.query.filter_by(username=new_uname).first() if prof: prof.username = new_uname prof.bio = bio prof.avatar_url = color db.session.commit() if request.sid in online_users: online_users[request.sid]['username'] = new_uname online_users[request.sid]['bio'] = bio online_users[request.sid]['avatar_color'] = color if old_uname and new_uname and old_uname != new_uname: emit('system_message', f"{old_uname} changed their name to {new_uname}", broadcast=True) emit('update_users', online_users, broadcast=True) @socketio.on('typing') def handle_typing(data): emit('user_typing', data, broadcast=True) @socketio.on('stop_typing') def handle_stop_typing(data): emit('user_stop_typing', data, broadcast=True) @socketio.on('add_reaction') def handle_add_reaction(data): msg_id = data.get('msg_id') user = data.get('username') emoji = data.get('emoji') existing = Reaction.query.filter_by(message_id=msg_id, username=user, emoji=emoji).first() if existing: db.session.delete(existing) else: db.session.add(Reaction(message_id=msg_id, username=user, emoji=emoji)) db.session.commit() msg = db.session.get(Message, msg_id) if msg: emit('message_updated', msg.to_dict(), broadcast=True) @socketio.on('edit_message') def handle_edit_message(data): msg_id = data.get('msg_id') new_text = data.get('text') user = data.get('username') msg = db.session.get(Message, msg_id) if msg and msg.username == user and not msg.is_deleted: msg.text = new_text msg.is_edited = True db.session.commit() export_chat_history() emit('message_updated', msg.to_dict(), broadcast=True) @socketio.on('delete_message') def handle_delete_message(data): msg_id = data.get('msg_id') user = data.get('username') msg = db.session.get(Message, msg_id) if msg and msg.username == user: Reaction.query.filter_by(message_id=msg_id).delete() msg.is_deleted = True msg.text = "" msg.media_data = "" db.session.commit() export_chat_history() emit('message_updated', msg.to_dict(), broadcast=True) @socketio.on('load_private_history') def handle_load_private_history(data): sender = data.get('sender') recipient = data.get('recipient') history = Message.query.filter( Message.is_private == True, ((Message.username == sender) & (Message.recipient == recipient)) | ((Message.username == recipient) & (Message.recipient == sender)) ).all() emit('load_private_history', [msg.to_dict() for msg in history]) @socketio.on('load_call_history') def handle_load_call_history(data): uname = data.get('username') calls = CallLog.query.filter( (CallLog.caller == uname) | (CallLog.receiver == uname) ).all() emit('load_call_history', [c.to_dict() for c in calls]) @socketio.on('send_global_message') def handle_send_global(data): now = datetime.now().strftime("%I:%M %p") msg_id = f"msg_{int(time.time() * 1000)}" file_type = data.get('file_type', 'text') media_data = data.get('media_data', '') if file_type != 'text' and media_data and not media_data.startswith('/uploads/'): media_data = save_media_file(media_data, file_type) new_msg = Message( id=msg_id, username=data.get('username', 'Anonymous'), text=data.get('text', ''), media_data=media_data, file_type=file_type, timestamp=now, status='sent', is_private=False, reply_to_id=data.get('reply_to_id') ) db.session.add(new_msg) db.session.commit() export_chat_history() emit('global_message', new_msg.to_dict(), broadcast=True) @socketio.on('send_private_message') def handle_send_private(data): now = datetime.now().strftime("%I:%M %p") sender_name = data.get('username') recipient_name = data.get('recipient_name') msg_id = f"msg_{int(time.time() * 1000)}" file_type = data.get('file_type', 'text') media_data = data.get('media_data', '') if file_type != 'text' and media_data and not media_data.startswith('/uploads/'): media_data = save_media_file(media_data, file_type) new_msg = Message( id=msg_id, username=sender_name, recipient=recipient_name, text=data.get('text', ''), media_data=media_data, file_type=file_type, timestamp=now, status='sent', is_private=True, reply_to_id=data.get('reply_to_id') ) db.session.add(new_msg) db.session.commit() export_chat_history() msg_dict = new_msg.to_dict() for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname in (sender_name, recipient_name): emit('private_message', msg_dict, room=sid) @socketio.on('ack_delivery') def handle_ack_delivery(data): msg_id = data.get('msg_id') sender = data.get('sender') msg = db.session.get(Message, msg_id) if msg and msg.status == 'sent': msg.status = 'delivered' db.session.commit() export_chat_history() for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == sender: emit('status_update', {'msg_id': msg_id, 'status': 'delivered'}, room=sid) latest_offers = {} @socketio.on('call_user') def handle_call_user(data): recipient = data.get('to') latest_offers[recipient] = data.get('offer') for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == recipient: emit('incoming_call', {'from': data.get('from')}, room=sid) @socketio.on('ready_for_offer') def handle_ready_for_offer(data): recipient = data.get('to') requester = online_users.get(request.sid) req_name = requester['username'] if isinstance(requester, dict) else requester offer = latest_offers.get(req_name) if offer: emit('call_offer_data', offer, room=request.sid) @socketio.on('answer_call') def handle_answer_call(data): recipient = data.get('to') for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == recipient: emit('call_answered', {'answer': data.get('answer')}, room=sid) @socketio.on('reject_call') def handle_reject_call(data): recipient = data.get('to') caller = data.get('from') now = datetime.now().strftime("%I:%M %p") call_id = f"call_{int(time.time() * 1000)}" call_log = CallLog(id=call_id, caller=caller, receiver=recipient, call_type='missed', duration='00:00', timestamp=now) db.session.add(call_log) db.session.commit() log_dict = call_log.to_dict() for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname in (caller, recipient): emit('new_call_log', log_dict, room=sid) for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == recipient: emit('call_ended', room=sid) @socketio.on('ice_candidate') def handle_ice_candidate(data): recipient = data.get('to') for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == recipient: emit('ice_candidate', {'candidate': data.get('candidate')}, room=sid) @socketio.on('end_call') def handle_end_call(data): recipient = data.get('to') duration = data.get('duration', '00:00') connected = data.get('connected', False) is_missed = data.get('missed', False) caller_name = "" for sid, userObj in online_users.items(): if sid == request.sid: caller_name = userObj['username'] if isinstance(userObj, dict) else userObj if caller_name: now = datetime.now().strftime("%I:%M %p") call_id = f"call_{int(time.time() * 1000)}" call_type = 'missed' if is_missed else 'outgoing' call_log = CallLog(id=call_id, caller=caller_name, receiver=recipient, call_type=call_type, duration=duration, timestamp=now) db.session.add(call_log) db.session.commit() log_dict = call_log.to_dict() for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname in (caller_name, recipient): emit('new_call_log', log_dict, room=sid) for sid, userObj in online_users.items(): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == recipient: emit('call_ended', room=sid) @socketio.on('admin_kick') def handle_admin_kick(data): if not session.get('is_admin'): return target = data.get('target_username') for sid, userObj in list(online_users.items()): uname = userObj['username'] if isinstance(userObj, dict) else userObj if uname == target: emit('kicked', room=sid) disconnect(sid) @socketio.on('admin_clear_history') def handle_admin_clear(): if not session.get('is_admin'): return Message.query.delete() Reaction.query.delete() CallLog.query.delete() db.session.commit() export_chat_history() emit('history_cleared', broadcast=True) if __name__ == '__main__': socketio.run(app, host='0.0.0.0', port=5000)