Browse Source

viré le bazar de coco, quelques renommages, ajouter une ébauche de possibilité pour recevoir des commandes directement depuis un socket, construction des modèles de bdd

master
hadware 6 years ago
parent
commit
f0ea764a13
  1. 3
      requirements.txt
  2. 22
      scormod.py
  3. 0
      scormod/__init__.py
  4. 0
      scormod/cli_apps/__init__.py
  5. 56
      scormod/client.py
  6. 5
      scormod/cmd_serv.py
  7. 60
      scormod/commons.py
  8. 0
      scormod/constants.py
  9. 66
      scormod/models.py
  10. 0
      scormod/processors/__init__.py
  11. 29
      scormod/processors/commons.py
  12. 20
      scormod/processors/connections.py
  13. 71
      scormod/processors/messages.py
  14. 31
      setup.py
  15. 1
      tools/coco/__init__.py
  16. 153
      tools/coco/client.py
  17. 115
      tools/coco/requests.py
  18. 93
      tools/coco/tools.py
  19. 29
      tools/processors/connections.py
  20. 148
      tools/processors/messages.py

3
requirements.txt

@ -1,2 +1,5 @@
bidict bidict
websockets websockets
mongoengine
dataclasses
python-dotenv

22
cocobot.py → scormod.py

@ -4,9 +4,9 @@ import argparse
import asyncio import asyncio
import logging import logging
from tools.base import CoboBot from scormod.client import ScorMod
from tools.processors import MessageDispatcher, CommandsDispatcherProcessor, ConnectionDispatcher from scormod.cmd_serv import CommandServer
from tools.processors.messages import * from scormod.processors.messages import *
# setting up argument parser # setting up argument parser
parser = argparse.ArgumentParser(description='Le lou bot') parser = argparse.ArgumentParser(description='Le lou bot')
@ -17,9 +17,6 @@ parser.add_argument('--port', type=int, help='port on which to connect the socke
parser.add_argument('--method', type=str, help='http or https', default="https") parser.add_argument('--method', type=str, help='http or https', default="https")
parser.add_argument('--verbose', help='print debug information', action='store_true') parser.add_argument('--verbose', help='print debug information', action='store_true')
# setting up coco client
cococlient = CocoClient()
# setting up the various dispatchers # setting up the various dispatchers
cmds = [cmd_class(cococlient) for cmd_class in (CocoConnectCommand, CocoMsgCommand, CocoListCommand, cmds = [cmd_class(cococlient) for cmd_class in (CocoConnectCommand, CocoMsgCommand, CocoListCommand,
CocoSwitchCommand, CocoQuitCommand, CocoBroadcastCommand)] CocoSwitchCommand, CocoQuitCommand, CocoBroadcastCommand)]
@ -32,6 +29,9 @@ root_messages_dispatcher = MessageDispatcher([coco_commands])
connections_dispatcher = ConnectionDispatcher([]) connections_dispatcher = ConnectionDispatcher([])
# TODO : add a TCP-socket-based command line interface
# https://asyncio.readthedocs.io/en/latest/tcp_echo.html
if __name__ == "__main__": if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
if args.verbose: if args.verbose:
@ -40,9 +40,9 @@ if __name__ == "__main__":
else: else:
logging.getLogger().setLevel(logging.INFO) logging.getLogger().setLevel(logging.INFO)
cocobot = CoboBot(args.cookie, args.channel, args.domain, args.port, args.method, scormod = ScorMod(args.cookie, args.channel, args.domain, args.port, args.method,
root_messages_dispatcher, connections_dispatcher, cococlient) root_messages_dispatcher, connections_dispatcher)
asyncio.get_event_loop().run_until_complete(cocobot.listen()) command_server = CommandServer()
loop = asyncio.get_event_loop()
loop.run_until_complete(scormod.run())

0
tools/__init__.py → scormod/__init__.py

0
scormod/cli_apps/__init__.py

56
tools/base.py → scormod/client.py

@ -1,23 +1,19 @@
import html import html
import json import json
import logging import logging
from asyncio import sleep
from asyncio import sleep, gather
import websockets import websockets
from tools.coco.client import CocoClient from tools.commons import AbstractMessage, Message, BotMessage, AttackCommand, Sound, UserList
from tools.commons import AbstractResponse, Message, BotMessage, AttackCommand, Sound, UserList
from tools.processors import MessageDispatcher, ConnectionDispatcher from tools.processors import MessageDispatcher, ConnectionDispatcher
class CoboBot: class ScorMod:
COCO_PULSE_TICK = 1 # number of seconds between each check
def __init__(self, cookie: str, channel: str, domain: str, port: int, method: str, def __init__(self, cookie: str, channel: str, domain: str, port: int, method: str,
messages_dispatcher: MessageDispatcher, messages_dispatcher: MessageDispatcher,
connect_dispatcher: ConnectionDispatcher, connect_dispatcher: ConnectionDispatcher):
cococlient: CocoClient):
# setting up variables required by the server. The default is a Kabutops on the main lou server, I think # setting up variables required by the server. The default is a Kabutops on the main lou server, I think
self.cookie = cookie self.cookie = cookie
@ -27,8 +23,7 @@ class CoboBot:
self.method = method self.method = method
self.msg_dispatch = messages_dispatcher self.msg_dispatch = messages_dispatcher
self.cnt_dispatch = connect_dispatcher self.cnt_dispatch = connect_dispatcher
self.user_list = None # type: UserList self.user_list: UserList = UserList()
self.cococlient = cococlient
async def _send_message(self, message): async def _send_message(self, message):
logging.debug("Sending message to server") logging.debug("Sending message to server")
@ -37,7 +32,7 @@ class CoboBot:
elif isinstance(message, bytes): elif isinstance(message, bytes):
await self.socket.send(message) await self.socket.send(message)
async def _dispatch_response(self, response_obj : AbstractResponse): async def _dispatch_response(self, response_obj : AbstractMessage):
if isinstance(response_obj, (Message, BotMessage, AttackCommand)): if isinstance(response_obj, (Message, BotMessage, AttackCommand)):
await self._send_message(response_obj.to_dict()) await self._send_message(response_obj.to_dict())
elif isinstance(response_obj, Sound): elif isinstance(response_obj, Sound):
@ -68,30 +63,29 @@ class CoboBot:
if isinstance(response, list): if isinstance(response, list):
for response_obj in response: for response_obj in response:
await self._dispatch_response(response_obj) await self._dispatch_response(response_obj)
elif isinstance(response, AbstractResponse): elif isinstance(response, AbstractMessage):
await self._dispatch_response(response) await self._dispatch_response(response)
async def socket_listener(self): async def socket_listener(self):
while True: async for msg in self.socket:
msg = await self.socket.recv() if isinstance(msg, bytes):
if type(msg) != bytes: logging.debug("Received sound file")
msg_data = json.loads(msg, encoding="utf-8") continue
msg_type = msg_data.get("type", "")
if msg_type == "userlist":
self.user_list = UserList(msg_data["users"])
logging.info(str(self.user_list))
elif msg_type == "msg": msg_data = json.loads(msg, encoding="utf-8")
await self._on_message(msg_data) msg_type = msg_data.get("type", "")
if msg_type == "userlist":
self.user_list.update(msg_data["users"])
logging.info(str(self.user_list))
elif msg_type == "connect": elif msg_type == "msg":
await self._on_connect(msg_data) await self._on_message(msg_data)
elif msg_type == "disconnect": elif msg_type == "connect":
await self._on_disconnect(msg_data) await self._on_connect(msg_data)
else: elif msg_type == "disconnect":
logging.debug("Received sound file") await self._on_disconnect(msg_data)
async def coco_pulse(self): async def coco_pulse(self):
while True: while True:
@ -102,10 +96,10 @@ class CoboBot:
if isinstance(new_messages, list): if isinstance(new_messages, list):
for response_obj in new_messages: for response_obj in new_messages:
await self._dispatch_response(response_obj) await self._dispatch_response(response_obj)
elif isinstance(new_messages, AbstractResponse): elif isinstance(new_messages, AbstractMessage):
await self._dispatch_response(new_messages) await self._dispatch_response(new_messages)
async def listen(self): async def run(self):
if self.method == "https": if self.method == "https":
socket_address = 'wss://%s/socket/%s' % (self.domain, self.channel) socket_address = 'wss://%s/socket/%s' % (self.domain, self.channel)
else: else:
@ -114,6 +108,6 @@ class CoboBot:
async with websockets.connect(socket_address, async with websockets.connect(socket_address,
extra_headers={"cookie": "id=%s" % self.cookie}) as websocket: extra_headers={"cookie": "id=%s" % self.cookie}) as websocket:
self.socket = websocket self.socket = websocket
await gather(self.socket_listener(), self.coco_pulse()) await self.socket_listener()

5
scormod/cmd_serv.py

@ -0,0 +1,5 @@
class CommandServer:
async def run(self):
pass

60
tools/commons.py → scormod/commons.py

@ -1,9 +1,16 @@
from pathlib import Path
from typing import Dict
from dataclasses import dataclass
class UserList: class UserList:
"""Wrapper around the 'currently connected users' dictionary""" """Wrapper around the 'currently connected users' dictionary"""
def __init__(self, userlist_data): def __init__(self):
self.users: Dict[str, Dict] = {}
def update(self, userlist_data):
self.users = {user_data["userid"]: user_data for user_data in userlist_data} self.users = {user_data["userid"]: user_data for user_data in userlist_data}
for id, user in self.users.items(): for id, user in self.users.items():
if "you" in user["params"] and user["params"]["you"]: if "you" in user["params"] and user["params"]["you"]:
@ -36,49 +43,60 @@ class UserList:
"%s" % "\n".join(["\t - %s" % self.name(user_id) for user_id in self.users]) "%s" % "\n".join(["\t - %s" % self.name(user_id) for user_id in self.users])
class AbstractResponse: class AbstractMessage:
pass pass
class Message(AbstractResponse):
def __init__(self, msg: str): @dataclass
self.msg = msg class Message(AbstractMessage):
msg: str
def to_dict(self): def to_dict(self):
return {"lang": "fr", "msg": self.msg, "type": "msg"} return {"lang": "fr", "msg": self.msg, "type": "msg"}
class BotMessage(AbstractResponse): @dataclass
class PrivateMessage(AbstractMessage):
msg: str
recipient_id: str
def __init__(self, msg: str): def to_dict(self):
self.msg = msg return {"msg": self.msg, "type": "private_msg", "userid": self.recipient_id}
@dataclass
class BotMessage(AbstractMessage):
msg: str
def to_dict(self): def to_dict(self):
return {"type": "bot", "msg": self.msg} return {"type": "bot", "msg": self.msg}
class Sound(AbstractResponse): @dataclass
class Sound(AbstractMessage):
def __init__(self, sound_filepath: str): filepath : Path
self.filepath = sound_filepath
def get_bytes(self): def get_bytes(self):
with open(self.filepath, "rb") as soundfile: with open(self.filepath, "rb") as soundfile:
return soundfile.read() return soundfile.read()
@dataclass
class AttackCommand(AbstractResponse): class AttackCommand(AbstractMessage):
target : str
def __init__(self, target_name: str, offset=1): offset: int = 1
self.target = target_name
self.offset = offset
def to_dict(self): def to_dict(self):
return {"target": self.target, "order": self.offset, "type": "attack"} return {"target": self.target, "order": self.offset, "type": "attack"}
class Sleep(AbstractResponse): @dataclass
class UserDataRequest(AbstractMessage):
user_id: str
def to_dict(self):
return {"type": "inspect", "user_id": self.user_id}
def __init__(self, duration : int): @dataclass
self.duration = duration class Sleep(AbstractMessage):
duration: int

0
tools/constants.py → scormod/constants.py

66
scormod/models.py

@ -0,0 +1,66 @@
from datetime import datetime
from ipaddress import ip_address
from mongoengine import Document, StringField, DateTimeField, IntField, ListField, ReferenceField, BooleanField
class IPField(IntField):
"""An IP field.
"""
def validate(self, value):
try:
ip_address(value)
except ValueError as err:
self.error(str(err))
def to_mongo(self, value):
return int(ip_address(value))
def to_python(self, value):
return ip_address(value)
def prepare_query_value(self, op, value):
return int(ip_address(value))
class UserConnect(Document):
cookie = StringField(required=True)
pokemon = StringField(required=True)
adjective = StringField(required=True)
ip = IPField(required=True)
time = DateTimeField(required=True, default=datetime.now)
class ChatMessage(Document):
cookie = StringField(required=True)
ip = IPField()
msg = StringField()
time = DateTimeField(required=True, default=datetime.now)
class UserProfile(Document):
cookie = StringField(required=True, primary_key=True)
pokemon = StringField(required=True)
adjective = StringField(required=True)
tags = ListField(StringField)
whitelisted = BooleanField(default=False)
class IPProfile(Document):
ip = IPField(required=True)
tags = ListField(StringField)
whitelisted = BooleanField(default=False)
class FilterRule(Document):
rule = StringField()
ip = StringField()
cookie = StringField()
action = StringField(default="mute")
# used when the action is "tag"
tag = StringField()
def match(self, s: str) -> bool:
pass

0
tools/processors/__init__.py → scormod/processors/__init__.py

29
tools/processors/commons.py → scormod/processors/commons.py

@ -1,5 +1,5 @@
import logging import logging
from typing import List from typing import List, Optional
from tools.commons import UserList from tools.commons import UserList
@ -26,7 +26,7 @@ class MessageDispatcher:
def __init__(self, processor_list : List[MessageProcessor]): def __init__(self, processor_list : List[MessageProcessor]):
self.processor_list = processor_list self.processor_list = processor_list
def dispatch(self,text : str, sender_id : str, users_list : UserList) -> str: def dispatch(self,text : str, sender_id : str, users_list : UserList) -> Optional[str]:
"""Tells its first botprocessor to match the message to process this message and returns its answer""" """Tells its first botprocessor to match the message to process this message and returns its answer"""
for processor in self.processor_list: for processor in self.processor_list:
if processor.match(text, sender_id, users_list): if processor.match(text, sender_id, users_list):
@ -34,3 +34,28 @@ class MessageDispatcher:
return processor.process(text, sender_id, users_list) return processor.process(text, sender_id, users_list)
return None return None
class ConnectProcessor:
def match(self, sender_id: str, users_list: UserList) -> bool:
"""Returns true if this is the kind of user connection a processor should respond to"""
pass
def process(self, sender_id: str, users_list: UserList) -> str:
"""Processes a message and returns an answer"""
pass
class ConnectionDispatcher:
def __init__(self, processor_list: List[ConnectProcessor]):
self.processor_list = processor_list
def dispatch(self, sender_id: str, users_list: UserList) -> Optional[str]:
"""Tells its first botprocessor to match the message to process this message and returns its answer"""
for processor in self.processor_list:
if processor.match(sender_id, users_list):
logging.info("Matched %s" % processor.__class__.__name__)
return processor.process(sender_id, users_list)
return None

20
scormod/processors/connections.py

@ -0,0 +1,20 @@
from .commons import ConnectProcessor
from ..commons import UserList
class BannedIPsProcessor(ConnectProcessor):
pass
class MutedIPsProcessor(ConnectProcessor):
pass
class LockdownModeProcessor(ConnectProcessor):
"""Matches any user not whitelisted when activated"""
def __init__(self):
activated = False
def match(self, sender_id: str, users_list: UserList) -> bool:
pass

71
scormod/processors/messages.py

@ -0,0 +1,71 @@
from typing import Type
from tools.coco.client import CocoClient
from tools.commons import Message, BotMessage
from tools.constants import AUTHORIZED_USERIDS
from .commons import *
class DispatcherBotProcessor(MessageProcessor):
"""A processor that matches a context, then forwards the message to a list of sub-processors.
This enables the botprocessor-matching mechanism to behave kinda like a decision tree"""
def __init__(self, processors_list: List[MessageProcessor]):
self.dispatcher = MessageDispatcher(processors_list)
def process(self, text: str, sender_id: str, users_list: UserList):
return self.dispatcher.dispatch(text, sender_id, users_list)
class CommandsDispatcherProcessor(DispatcherBotProcessor):
"""Reacts to commands of the form '/botname command' or 'botname, command' """
def __init__(self, processors_list: List[MessageProcessor], trigger_word: str = None, default_response: str = None):
super().__init__(processors_list)
self.trigger = trigger_word
self.default_response = default_response if default_response is not None else "Commande non reconnue, pd"
def match(self, text: str, sender_id: str, users_list: UserList):
trigger = self.trigger.upper() if self.trigger is not None else users_list.my_name.upper()
return text.upper().startswith(trigger + ",") \
or text.upper().startswith("/" + trigger)
def process(self, text: str, sender_id: str, users_list: UserList):
without_cmd = text[len(self.trigger) + 1:]
response = super().process(without_cmd, sender_id, users_list)
return Message(self.default_response) if response is None else response
def admin_command(klass: Type[MessageProcessor]):
pass
@admin_command
class LockDownCommandProcessor:
pass
@admin_command
class TaggerCommandProcessor:
"""Adds tagging rules based on a word filtering rule"""
pass
class TaggerProcessor:
"""Tags IP's based on word filtering rule"""
pass
class BotHelp(MessageProcessor):
"""Displays the help string for all processors in the list that have a helpt string"""
def __init__(self, processors_list: List[BaseCocobotCommand]):
all_help_strs = [proc.HELP_STR
for proc in processors_list if proc.HELP_STR is not None]
self.help_str = ", ".join(all_help_strs)
def match(self, text: str, sender_id: str, users_list: UserList):
return text.lower().startswith("help")
def process(self, text: str, sender_id: str, users_list: UserList):
return BotMessage(self.help_str)

31
setup.py

@ -0,0 +1,31 @@
from setuptools import setup, find_packages
with open("README.md") as readme:
long_description = readme.read()
with open("requirements.txt") as reqs:
requirements = reqs.read().split("\n")
setup(
name='scormod',
version='0.1',
description="A moderation bot for the loult",
long_description=long_description,
long_description_content_type='text/markdown',
author='hadware',
license='MIT',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: AGPL License',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
keywords='cookies',
packages=find_packages(),
install_requires=requirements,
include_package_data=True,
test_suite='nose.collector',
tests_require=['nose'])

1
tools/coco/__init__.py

@ -1 +0,0 @@
from .client import CocoClient

153
tools/coco/client.py

@ -1,153 +0,0 @@
import logging
import random
from typing import List, Dict, Tuple, Union, Set
from collections import defaultdict
from .requests import LoginRequest, PostLoginRequest, PulseRequest, SendMsgRequest
from ..commons import BotMessage, Message, AbstractResponse
class Interlocutor:
def __init__(self, nick: str, age: int, city: str, is_male: bool, conv_id: str):
self.nick = nick
self.age = age
self.is_male = is_male
self.city = city
self.id = conv_id
@classmethod
def from_string(cls, str):
# 47130922100412004Rastapopoulos
# 47 (age) 1 (sexe) 30922 (city id) 100412(conv id)
age = int(str[:2])
is_male = int(str[2:3]) in (1, 6)
city_id = str[3:8]
conv_id = str[8:14]
nick = str[17:]
return cls(nick, age, city_id, is_male, conv_id)
def to_botmessage(self):
sex_indic = "un homme" if self.is_male else "une femme"
return BotMessage("Conversation avec %s, %s de %i ans" %(self.nick, sex_indic, self.age))
def __eq__(self, other):
return other.nick == self.nick
def __hash__(self):
return hash(self.nick)
class CocoClient:
def __init__(self):
self.interlocutors = set() # type: Set[Interlocutor]
self.current_interlocutor = None # type: Interlocutor
self.histories = defaultdict(list) # type:defaultdict[Interlocutor,List[Tuple]]
self.user_id = None # type:str
self.user_pass = None # type:str
self.nick = None # type:str
self.is_connected = False
def _format_history(self, interlocutor: Interlocutor):
if interlocutor in self.histories:
return [BotMessage("💬 %s: %s" % (nick, msg))
for nick, msg in self.histories[interlocutor][-5:]]
else:
return []
def __process_and_format_received_msg(self, received_msgs):
out = []
for user_code, msg in received_msgs:
user = Interlocutor.from_string(user_code)
self.interlocutors.add(user)
self.histories[user].append((user.nick, msg))
logging.info("Msg from %s : %s" % (user.nick, msg))
if self.current_interlocutor is not None and user == self.current_interlocutor:
out.append(Message("💬 %s: %s" % (user.nick, msg)))
else:
out.append(BotMessage("💬 %s: %s" % (user.nick, msg)))
return out
def disconnect(self):
self.interlocutors = set()
self.histories = defaultdict(list)
self.current_interlocutor = None
self.is_connected = False
self.nick = None
def connect(self, nick: str, age: int, is_female: bool, zip_code: str):
self.disconnect()
self.nick = nick
login_req = LoginRequest(nick, age, is_female, zip_code)
self.user_id, self.user_pass = login_req.retrieve()
logging.info("Logged in to coco as %s" % self.nick)
post_login_req = PostLoginRequest(self.user_id, self.user_pass)
try:
if post_login_req.retrieve():
logging.info("Post login successful")
self.is_connected = True
else:
logging.info("Post login failed")
except ZeroDivisionError:
logging.info("Message cipher failed")
def pulse(self) -> List[AbstractResponse]:
pulse_req = PulseRequest(self.user_id, self.user_pass)
received_msg = pulse_req.retrieve()
return self.__process_and_format_received_msg(received_msg)
def send_msg(self, msg: str) -> List[AbstractResponse]:
if self.current_interlocutor is not None:
sendmsg_req = SendMsgRequest(self.user_id, self.user_pass, self.current_interlocutor.id, msg)
output = sendmsg_req.retrieve()
self.histories[self.current_interlocutor].append((self.nick, msg))
out_msg = Message("💬 %s: %s" % (self.nick, msg))
out = [out_msg]
if output:
out += self.__process_and_format_received_msg(output)
return out
else:
return [BotMessage("Il faut sélectionner une conversation d'abord pd")]
def broadcast_msg(self, msg: str) -> List[AbstractResponse]:
if self.interlocutors:
outputs = [Message("💬 %s à tous: %s" % (self.nick, msg))]
for interlocutor in self.interlocutors:
sendmsg_req = SendMsgRequest(self.user_id, self.user_pass, interlocutor.id, msg)
output = sendmsg_req.retrieve()
self.histories[interlocutor].append((self.nick, msg))
if output:
outputs += self.__process_and_format_received_msg(output)
return outputs
else:
return [BotMessage("Aucune conversation active")]
def switch_conv(self, nick: str=None) -> Union[List[BotMessage], BotMessage]:
if not self.interlocutors:
return BotMessage("Pas de conversations en cours")
new_interlocutor = None
if nick is not None:
for usr in self.interlocutors:
if usr.nick.upper() == nick.upper():
new_interlocutor = usr
break
else:
new_interlocutor = random.choice(list(self.interlocutors))
if new_interlocutor is None:
return BotMessage("Impossible de trouver l'utilisateur")
else:
self.current_interlocutor = new_interlocutor
return [new_interlocutor.to_botmessage()] + \
self._format_history(self.current_interlocutor)
def list_convs(self):
return BotMessage("Conversations : " + ", ".join(["%s(%i)" % (usr.nick, usr.age)
for usr in self.interlocutors]))

115
tools/coco/requests.py

@ -1,115 +0,0 @@
import logging
from urllib.request import Request, urlopen
from random import randint, choice, random
from string import ascii_uppercase
from typing import Tuple, List
import re
from .tools import get_city_id, coco_cipher, encode_msg, decode_msg
class BaseCocoRequest:
host = 'http://cloud.coco.fr/'
headers = {'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0',
'Cookie': '_ga=GA1.2.795717920.1518381607',
'Host': 'cloud.coco.fr',
'Referer': 'http://www.coco.fr/chat/index.html'}
def _get_url(self):
pass
def _parse_response(self, response):
pass
def retrieve(self):
req = Request(self._get_url(), headers=self.headers)
response = urlopen(req).read()
cleaned = response.decode("utf-8")[len("process1('#"):-len("');")]
return self._parse_response(cleaned)
class LoginRequest(BaseCocoRequest):
def __init__(self, nick: str, age: int, is_female: bool, zip_code: str):
self.nick = nick
self.age = str(age)
self.sex = str('2' if is_female else '1')
self.city = get_city_id(zip_code)
def _get_url(self):
identifier = str(randint(100000000, 990000000)) + '0' + ''.join(choice(ascii_uppercase) for i in range(20))
return self.host + '40' + self.nick + '*' + self.age + self.sex + self.city + identifier
def _parse_response(self, response):
credentials = response[2:14]
return credentials[:6], credentials[6:] # user_id and password
class LoggedInRequest(BaseCocoRequest):
def __init__(self, user_id, password):
self.user_id = user_id
self.password = password
@property
def token(self):
return self.user_id + self.password
class PostLoginRequest(LoggedInRequest):
client_id_str = "3551366741*0*1aopiig*-940693579*192.168.0.14*0"
def _get_url(self):
return self.host + '52' + self.token + coco_cipher(self.client_id_str, self.password)
def _parse_response(self, response : str):
"""Checks if the post-login was successful"""
logging.debug(response)
return response.startswith("99556")
class PulseRequest(LoggedInRequest):
user_match = re.compile(r"[0-9]{17}[A-Za-z0-9]+")
def _get_url(self):
return self.host + "95" + self.token + "?" + str(random())
def _parse_response(self, response):
"""Can either be a single message or several messages"""
if response == "Z":
return []
response = response[3:] # cutting the 669
split = response.split("#")
it = iter(split)
output_msgs = [] # type: List[Tuple[str,str]]
while True:
try:
current = next(it)
if re.match(self.user_match, current):
msg = next(it)
decoded_msg = decode_msg(msg)
output_msgs.append((current, decoded_msg))
except StopIteration:
break
return output_msgs
class SendMsgRequest(PulseRequest):
def __init__(self, user_id, password, conv_id: str, msg: str):
super().__init__(user_id, password)
self.conv_id = conv_id
self.msg = msg
def _get_url(self):
return self.host + "99" + self.token + self.conv_id + str(randint(0, 6)) + encode_msg(self.msg)
def _parse_response(self, response: str):
"""Response to a send message request can either just be #97x or an
actual message (like in pulse request)"""
if response.startswith("97"):
return []
else:
return super()._parse_response(response)

93
tools/coco/tools.py

@ -1,93 +0,0 @@
from urllib.request import urlopen
import re
import bidict
doc = {0: 65, 1: 66, 2: 67, 3: 68, 4: 69, 5: 70, 6: 71, 7: 72, 8: 73, 9: 74, 10: 75, 11: 76, 12: 77, 13: 78, 14: 79,
15: 80, 16: 81, 17: 82, 18: 83, 19: 84, 20: 101, 21: 102, 22: 103, 23: 104, 24: 105, 25: 106, 26: 107,
27: 108, 28: 109, 29: 110, 30: 85, 31: 86, 32: 87, 33: 88, 34: 89, 35: 90, 36: 97, 37: 98, 38: 99, 39: 100,
40: 111, 41: 112, 42: 113, 43: 114, 44: 115, 45: 116, 46: 117, 47: 118, 48: 119, 49: 120, 50: 121, 51: 122,
52: 48, 53: 49, 54: 50, 55: 51, 56: 52, 57: 53, 58: 54, 59: 55, 60: 56, 61: 57, 62: 43, 63: 47, 64: 61}
def coco_cipher(str: str, key: str):
"""Implementation of coco's weird 'enxo' cipher. key has to be the user's password,
retrieved from a previous request"""
def none_int(var):
return var if var is not None else 0
def safe_get_charcode(s: str, idx: int):
try:
return ord(s[idx])
except IndexError:
return None
output, chr1, chr2, chr3 = "", 0, 0, 0
enc, revo = {}, {}
for j in range(65):
revo[doc[j]] = j
result = ""
for i, char_n in enumerate(str):
result += chr(ord(key[i % len(key)]) ^ ord(char_n))
i = 0
while i < len(str):
chr1 = safe_get_charcode(result, i)
i += 1
chr2 = safe_get_charcode(result, i)
i += 1
chr3 = safe_get_charcode(result, i)
i += 1
enc[0] = none_int(chr1) >> 2
enc[1] = ((none_int(chr1) & 3) << 4) | (none_int(chr2) >> 4)
enc[2] = ((none_int(chr2) & 15) << 2) | (none_int(chr3) >> 6)
enc[3] = none_int(chr3) & 63
if chr2 is None:
enc[2] = 64
enc[3] = 64
elif chr3 is None:
enc[3] = 64
for j in range(4):
output += chr(doc[enc[j]])
return output
def get_city_id(postal_code: str):
response = str(urlopen("http://www.coco.fr/cocoland/%s.js" % postal_code).read(), 'utf-8')
first_city_code = re.search(r'[0-9]+', response)
if first_city_code:
return first_city_code.group()
elif 'ERROR' in response:
raise ValueError('Invalid postal code')
else:
RuntimeError('Unexpected output')
smilies = [":)", ":(", ";)", ":d", ":-o", ":s", ":$", "*-)", "-)", "^o)", ":p", "(l)", "(v)", ":'(", "(h)", "(f)",
":@", "(y)", "(n)", "(k)", "gr$", "(a)", "(6)", "(yn)", "+o(", "na$", "oh$", "tr$", "(e)", "sh$", "fu$",
"nw$", "ba$", "ao$", "db$", "si$", "oo$", "co$", "bi$", "cc$", "ye$", "mo$", "aa$", "ci$", "uu$", "ff$",
"zz$", "gt$", "ah$", "mm$", "?$", "xx$"]
special_chars = bidict.bidict({" ": "_", "$": "*7", "%": "*g", "'": "*8", "(": "(", ")": ")", "*": "*s", "=": "*h",
"?": "=", "@": "*m", "^": "*l", "_": "*0", "": "*d", "à": "*a", "â": "*k", "ç": "*c", "è": "*e",
"é": "*r", "ê": "*b", "î": "*i", "ï": "*j", "ô": "*o", "ù": "*f", "û": "*u"})
def encode_msg(msg : str):
"""Encoding the message to coco's weird url-compatible format"""
for i in range(len(smilies)):
msg = msg.replace(smilies[i], ';' + str(i).zfill(2))
for char, replacement in special_chars.items():
msg = msg.replace(char, replacement)
msg = ''.join([c for c in msg if ord(c) < 128])
return msg
def decode_msg(msg: str):
"""Decoding coco's weird message format"""
for coded, char in special_chars.inv.items():
msg = msg.replace(coded, char)
return msg

29
tools/processors/connections.py

@ -1,29 +0,0 @@
import logging
from typing import List
from tools.commons import UserList
class ConnectProcessor:
def match(self, sender_id: str, users_list: UserList) -> bool:
"""Returns true if this is the kind of user connection a processor should respond to"""
pass
def process(self, sender_id: str, users_list: UserList) -> str:
"""Processes a message and returns an answer"""
pass
class ConnectionDispatcher:
def __init__(self, processor_list: List[ConnectProcessor]):
self.processor_list = processor_list
def dispatch(self, sender_id: str, users_list: UserList) -> str:
"""Tells its first botprocessor to match the message to process this message and returns its answer"""
for processor in self.processor_list:
if processor.match(sender_id, users_list):
logging.info("Matched %s" % processor.__class__.__name__)
return processor.process(sender_id, users_list)
return None

148
tools/processors/messages.py

@ -1,148 +0,0 @@
from tools.coco.client import CocoClient
from tools.commons import Message, BotMessage
from tools.constants import AUTHORIZED_USERIDS
from .commons import *
class DispatcherBotProcessor(MessageProcessor):
"""A processor that matches a context, then forwards the message to a list of sub-processors.
This enables the botprocessor-matching mechanism to behave kinda like a decision tree"""
def __init__(self, processors_list: List[MessageProcessor]):
self.dispatcher = MessageDispatcher(processors_list)
def process(self, text: str, sender_id: str, users_list: UserList):
return self.dispatcher.dispatch(text, sender_id, users_list)
class CommandsDispatcherProcessor(DispatcherBotProcessor):
"""Reacts to commands of the form '/botname command' or 'botname, command' """
def __init__(self, processors_list: List[MessageProcessor], trigger_word: str = None, default_response: str = None):
super().__init__(processors_list)
self.trigger = trigger_word
self.default_response = default_response if default_response is not None else "Commande non reconnue, pd"
def match(self, text: str, sender_id: str, users_list: UserList):
trigger = self.trigger.upper() if self.trigger is not None else users_list.my_name.upper()
return text.upper().startswith(trigger + ",") \
or text.upper().startswith("/" + trigger)
def process(self, text: str, sender_id : str, users_list: UserList):
without_cmd = text[len(self.trigger)+1:]
response = super().process(without_cmd, sender_id, users_list)
return Message(self.default_response) if response is None else response
class BaseCocobotCommand(MessageProcessor):
HELP_STR = None
_cmd_suffix = ""
def __init__(self, cococlient: CocoClient):
self.cococlient = cococlient
def match(self, text : str, sender_id : str, users_list : UserList):
return text.lower().startswith(self._cmd_suffix)
class CocoConnectCommand(BaseCocobotCommand):
HELP_STR = "/coconnect pseudo age code_postal"
_cmd_suffix = "nnect"
def process(self, text : str, sender_id : str, users_list : UserList):
text = text[len(self._cmd_suffix):].strip()
try:
nick, age, zip_code = text.split()
nick = ''.join([c for c in nick if ord(c) < 128]) # removing non-utf8 chars
except ValueError:
return Message("Pas le bon nombre d'arguments, pd")
if not nick.isalnum():
return Message("Le pseudo doit être alphanumérique, pd")
if len(age) != 2 or not age.isnumeric():
return Message("L'âge c'est avec DEUX chiffres (déso bulbi)")
if int(age) < 15:
return Message("L'âge minimum c'est 15 ans (déso bubbi)")
if len(zip_code) != 5 or not zip_code.isnumeric():
return Message("Le code postal c'est 5 chiffres, pd")
try:
self.cococlient.connect(nick, int(age), True, zip_code)
except ValueError:
return Message("Le code postal a pas l'air d'être bon")
if self.cococlient.is_connected:
return BotMessage("Connecté en tant que %s, de %s ans" % (nick, age))
else:
return BotMessage("La connection a chié, déswe")
class CocoMsgCommand(BaseCocobotCommand):
HELP_STR = "/cocospeak message"
_cmd_suffix = "speak"
def process(self, text : str, sender_id : str, users_list : UserList):
text = text[len(self._cmd_suffix):].strip()
return self.cococlient.send_msg(text)
class CocoBroadcastCommand(BaseCocobotCommand):
HELP_STR = "/cocoall message"
_cmd_suffix = "all"
def process(self, text : str, sender_id : str, users_list : UserList):
text = text[len(self._cmd_suffix):].strip()
return self.cococlient.broadcast_msg(text)
class CocoSwitchCommand(BaseCocobotCommand):
HELP_STR = "/cocoswitch [pseudo de l'interlocuteur]"
_cmd_suffix = "switch"
def process(self, text : str, sender_id : str, users_list : UserList):
text = text[len(self._cmd_suffix):].strip()
if text:
return self.cococlient.switch_conv(text)
else:
return self.cococlient.switch_conv()
class CocoListCommand(BaseCocobotCommand):
HELP_STR = "/cocolist"
_cmd_suffix = "list"
def process(self, text : str, sender_id : str, users_list : UserList):
return self.cococlient.list_convs()
class CocoQuitCommand(BaseCocobotCommand):
HELP_STR = "/cocoquit"
_cmd_suffix = "quit"
def match(self, text : str, sender_id : str, users_list : UserList):
return super().match(text, sender_id, users_list) and sender_id in AUTHORIZED_USERIDS
def process(self, text : str, sender_id : str, users_list : UserList):
self.cococlient.disconnect()
return BotMessage("Déconnecté!")
class BotHelp(MessageProcessor):
"""Displays the help string for all processors in the list that have a helpt string"""
def __init__(self, processors_list: List[BaseCocobotCommand]):
all_help_strs = [proc.HELP_STR
for proc in processors_list if proc.HELP_STR is not None]
self.help_str = ", ".join(all_help_strs)
def match(self, text : str, sender_id : str, users_list : UserList):
return text.lower().startswith("help")
def process(self, text : str, sender_id : str, users_list : UserList):
return BotMessage(self.help_str)
Loading…
Cancel
Save