# Written by retoor@molodetz.nl

# This source code implements a WebSocket-based RPC (Remote Procedure Call) view that uses asynchronous methods to facilitate real-time communication and services for an authenticated user session in a web application. The class handles WebSocket events, user authentication, and various RPC interactions such as login, message retrieval, and more.

# External imports are used from the aiohttp library for the WebSocket response handling and the snek.system view for the BaseView class.

# MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions.


from aiohttp import web  
from snek.system.view import BaseView
import traceback
import json
from snek.system.model import now 

class RPCView(BaseView):

    class RPCApi:
        def __init__(self, view, ws):
            self.view = view 
            self.app = self.view.app
            self.services = self.app.services
            self.ws = ws 

        @property
        def user_uid(self):
            return self.view.session.get("uid")

        @property 
        def request(self):
            return self.view.request 
        
        def _require_login(self):
            if not self.is_logged_in:
                raise Exception("Not logged in")

        @property 
        def is_logged_in(self):
            return self.view.session.get("logged_in", False)

        async def login(self, username, password):
            success = await self.services.user.validate_login(username, password)
            if not success:
                raise Exception("Invalid username or password")
            user = await self.services.user.get(username=username)
            self.view.session["uid"] = user["uid"]
            self.view.session["logged_in"] = True
            self.view.session["username"] = user["username"]
            self.view.session["user_nick"] = user["nick"]
            record = user.record
            del record['password']
            del record['deleted_at']
            await self.services.socket.add(self.ws,self.view.request.session.get('uid'))
            async for subscription in self.services.channel_member.find(user_uid=self.view.request.session.get("uid"), deleted_at=None, is_banned=False):
                await self.services.socket.subscribe(self.ws, subscription["channel_uid"], self.view.request.session.get("uid"))
            return record 

        async def search_user(self, query): 
            self._require_login()
            return [user['username'] for user in await self.services.user.search(query)]

        async def get_user(self, user_uid):
            self._require_login()
            if not user_uid:
                user_uid = self.user_uid
            user = await self.services.user.get(uid=user_uid)
            record = user.record
            del record['password']
            del record['deleted_at']
            if user_uid != user["uid"]:
                del record['email']
            return record 

        async def get_messages(self, channel_uid, offset=0,timestamp = None):
            self._require_login()
            messages = []
            for message in await self.services.channel_message.offset(channel_uid, offset or 0,timestamp or None):
                extended_dict = await self.services.channel_message.to_extended_dict(message)
                messages.append(extended_dict)
            return messages

        async def get_channels(self):
            self._require_login()
            channels = []
            async for subscription in self.services.channel_member.find(user_uid=self.user_uid, is_banned=False):
                channel = await self.services.channel.get(uid=subscription['channel_uid'])
                channels.append({
                    "name": subscription["label"],
                    "uid": subscription["channel_uid"],
                    "tag": channel["tag"],
                    "is_moderator": subscription["is_moderator"],
                    "is_read_only": subscription["is_read_only"]
                })
            return channels

        async def send_message(self, channel_uid, message):
            self._require_login()
            await self.services.chat.send(self.user_uid, channel_uid, message)
            return True 

        async def echo(self, *args):
            self._require_login()
            return args

        async def query(self, *args):
            self._require_login()
            query = args[0] 
            lowercase = query.lower()
            if any(keyword in lowercase for keyword in ["drop", "alter", "update", "delete", "replace", "insert", "truncate"]) and 'select' not in lowercase:
                raise Exception("Not allowed")
            return [dict(record) async for record in self.services.channel.query(args[0])]

        async def __call__(self, data):
            try:
                call_id = data.get("callId")
                method_name = data.get("method")
                if method_name.startswith("_"):
                    raise Exception("Not allowed")
                args = data.get("args") or []
                if hasattr(super(), method_name) or not hasattr(self, method_name):
                    return await self._send_json({"callId": call_id, "data": "Not allowed"})
                method = getattr(self, method_name.replace(".", "_"), None)
                if not method:
                    raise Exception("Method not found")
                success = True 
                try:
                    result = await method(*args)
                except Exception as ex:
                    result = {"exception": str(ex), "traceback": traceback.format_exc()}
                    success = False
                if result != "noresponse":
                    await self._send_json({"callId": call_id, "success": success, "data": result})
            except Exception as ex:
                await self._send_json({"callId": call_id, "success": False, "data": str(ex)})

        async def _send_json(self, obj):
            await self.ws.send_str(json.dumps(obj, default=str))
    

        async def get_online_users(self, channel_uid):
            self._require_login()
            
            return [dict(uid=record['uid'],username=record['username'], nick=record['nick'],last_ping=record['last_ping']) async for record in self.services.channel.get_online_users(channel_uid)]

        async def echo(self, obj):
            await self.ws.send_json(obj)
            return "noresponse"

        async def get_users(self, channel_uid):
            self._require_login()

            return [dict(uid=record['uid'],username=record['username'], nick=record['nick'],last_ping=record['last_ping']) async for record in self.services.channel.get_users(channel_uid)]

        async def ping(self, callId, *args):
            if self.user_uid:
                user = await self.services.user.get(uid=self.user_uid)
                user['last_ping'] = now()
                await self.services.user.save(user)
            return {"pong": args}

    async def get(self):
        ws = web.WebSocketResponse()
        await ws.prepare(self.request)
        if self.request.session.get("logged_in"):
            await self.services.socket.add(ws, self.request.session.get("uid"))
            async for subscription in self.services.channel_member.find(user_uid=self.request.session.get("uid"), deleted_at=None, is_banned=False):
                await self.services.socket.subscribe(ws, subscription["channel_uid"], self.request.session.get("uid"))
        rpc = RPCView.RPCApi(self, ws)
        async for msg in ws:
            if msg.type == web.WSMsgType.TEXT:
                try:
                    await rpc(msg.json())
                except Exception as ex:
                    await self.services.socket.delete(ws)
                    break
            elif msg.type == web.WSMsgType.ERROR:
                pass 
            elif msg.type == web.WSMsgType.CLOSE:
                pass 
        return ws
