mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Add grapevine protocol
This commit is contained in:
parent
2c607b5bb3
commit
ceb7d7bd26
1 changed files with 372 additions and 0 deletions
372
evennia/server/portal/grapevine.py
Normal file
372
evennia/server/portal/grapevine.py
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
"""
|
||||
Grapevine network connection
|
||||
|
||||
This is an implementation of the Grapevine Websocket protocol v 1.0.0 as
|
||||
outlined here: https://grapevine.haus/docs
|
||||
|
||||
This will allow the linked game to transfer status as well as connects
|
||||
the grapevine client to in-game channels.
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from twisted.internet import protocol
|
||||
from django.conf import settings
|
||||
from evennia.server.session import Session
|
||||
from evennia.utils import get_evennia_version
|
||||
from evennia.utils.logger import log_info, log_err
|
||||
from autobahn.twisted.websocket import (
|
||||
WebSocketClientProtocol, WebSocketClientFactory, connectWS)
|
||||
|
||||
# There is only one at this time
|
||||
GRAPEVINE_URI = "wss://grapevine.haus/socket"
|
||||
|
||||
GRAPEVINE_CLIENT_ID = settings.GRAPEVINE_CLIENT_ID
|
||||
GRAPEVINE_CLIENT_SECRET = settings.GRAPEVINE_CLIENT_SECRET
|
||||
GRAPEVINE_CHANNELS = settings.GRAPEVINE_CHANNELS
|
||||
|
||||
# defined error codes
|
||||
CLOSE_NORMAL = 1000
|
||||
GRAPEVINE_AUTH_ERROR = 4000
|
||||
GRAPEVINE_HEARTBEAT_FAILURE = 4001
|
||||
|
||||
|
||||
class RestartingWebsocketServerFactory(WebSocketClientFactory,
|
||||
protocol.ReconnectingClientFactory):
|
||||
"""
|
||||
A variant of the websocket-factory that auto-reconnects.
|
||||
|
||||
"""
|
||||
|
||||
initialDelay = 1
|
||||
factor = 1.5
|
||||
maxDelay = 60
|
||||
|
||||
def __init__(self, sessionhandler, *args, **kwargs):
|
||||
|
||||
self.uid = kwargs.pop('uid')
|
||||
self.channel = kwargs.pop('grapevine_channel')
|
||||
self.sessionhandler = sessionhandler
|
||||
|
||||
# self.noisy = False
|
||||
self.port = None
|
||||
self.bot = None
|
||||
|
||||
WebSocketClientFactory.__init__(self, GRAPEVINE_URI, *args, **kwargs)
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Build new instance of protocol
|
||||
|
||||
Args:
|
||||
addr (str): Not used, using factory/settings data
|
||||
|
||||
"""
|
||||
protocol = GrapevineClient()
|
||||
protocol.factory = self
|
||||
protocol.channel = self.channel
|
||||
protocol.sessionhandler = self.sessionhandler
|
||||
return protocol
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
"""
|
||||
Tracks reconnections for debugging.
|
||||
|
||||
Args:
|
||||
connector (Connector): Represents the connection.
|
||||
|
||||
"""
|
||||
log_info("(re)connecting to grapevine channel '%s'" % self.channel)
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
"""
|
||||
Called when Client failed to connect.
|
||||
|
||||
Args:
|
||||
connector (Connection): Represents the connection.
|
||||
reason (str): The reason for the failure.
|
||||
|
||||
"""
|
||||
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
"""
|
||||
Called when Client loses connection.
|
||||
|
||||
Args:
|
||||
connector (Connection): Represents the connection.
|
||||
reason (str): The reason for the failure.
|
||||
|
||||
"""
|
||||
if not (self.bot or (self.bot and self.bot.stopping)):
|
||||
self.retry(connector)
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
Force a reconnection of the bot protocol. This requires
|
||||
de-registering the session and then reattaching a new one,
|
||||
otherwise you end up with an ever growing number of bot
|
||||
sessions.
|
||||
|
||||
"""
|
||||
self.bot.stopping = True
|
||||
self.bot.transport.loseConnection()
|
||||
self.sessionhandler.server_disconnect(self.bot)
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"Connect protocol to remote server"
|
||||
|
||||
try:
|
||||
from twisted.internet import ssl
|
||||
except ImportError:
|
||||
log_err("To use Grapevine, The PyOpenSSL module must be installed.")
|
||||
else:
|
||||
context_factory = ssl.ClientContextFactory() if self.isSecure else None
|
||||
connectWS(self, context_factory)
|
||||
# service.name = "websocket/grapevine"
|
||||
# self.sessionhandler.portal.services.addService(service)
|
||||
|
||||
|
||||
class GrapevineClient(WebSocketClientProtocol, Session):
|
||||
"""
|
||||
Implements the grapevine client
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
WebSocketClientProtocol.__init__(self)
|
||||
Session.__init__(self)
|
||||
self.restart_downtime = None
|
||||
|
||||
def at_login(self):
|
||||
pass
|
||||
|
||||
def onOpen(self):
|
||||
"""
|
||||
Called when connection is established.
|
||||
|
||||
"""
|
||||
self.restart_downtime = None
|
||||
self.restart_task = None
|
||||
|
||||
self.stopping = False
|
||||
self.factory.bot = self
|
||||
|
||||
self.init_session("grapevine", GRAPEVINE_URI, self.factory.sessionhandler)
|
||||
self.uid = int(self.factory.uid)
|
||||
self.logged_in = True
|
||||
self.sessionhandler.connect(self)
|
||||
|
||||
self.send_authenticate()
|
||||
|
||||
def onMessage(self, payload, isBinary):
|
||||
"""
|
||||
Callback fired when a complete WebSocket message was received.
|
||||
|
||||
Args:
|
||||
payload (bytes): The WebSocket message received.
|
||||
isBinary (bool): Flag indicating whether payload is binary or
|
||||
UTF-8 encoded text.
|
||||
|
||||
"""
|
||||
if not isBinary:
|
||||
data = json.loads(str(payload, 'utf-8'))
|
||||
self.data_in(data=data)
|
||||
self.retry_task = None
|
||||
|
||||
def onClose(self, wasClean, code=None, reason=None):
|
||||
"""
|
||||
This is executed when the connection is lost for whatever
|
||||
reason. it can also be called directly, from the disconnect
|
||||
method.
|
||||
|
||||
Args:
|
||||
wasClean (bool): ``True`` if the WebSocket was closed cleanly.
|
||||
code (int or None): Close status as sent by the WebSocket peer.
|
||||
reason (str or None): Close reason as sent by the WebSocket peer.
|
||||
|
||||
"""
|
||||
self.disconnect(reason)
|
||||
|
||||
if code == GRAPEVINE_HEARTBEAT_FAILURE:
|
||||
log_err("Grapevine connection lost (Heartbeat error)")
|
||||
elif code == GRAPEVINE_AUTH_ERROR:
|
||||
log_err("Grapevine connection lost (Auth error)")
|
||||
elif self.restart_downtime:
|
||||
# server previously warned us about downtime and told us to be
|
||||
# ready to reconnect.
|
||||
log_info("Grapevine connection lost (Server restart).")
|
||||
|
||||
def _send_json(self, data):
|
||||
"""
|
||||
Send (json-) data to client.
|
||||
|
||||
Args:
|
||||
data (str): Text to send.
|
||||
|
||||
"""
|
||||
return self.sendMessage(json.dumps(data).encode('utf-8'))
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
|
||||
Args:
|
||||
reason (str or None): Motivation for the disconnection.
|
||||
|
||||
"""
|
||||
self.sessionhandler.disconnect(self)
|
||||
# autobahn-python: 1000 for a normal close, 3000-4999 for app. specific,
|
||||
# in case anyone wants to expose this functionality later.
|
||||
#
|
||||
# sendClose() under autobahn/websocket/interfaces.py
|
||||
self.sendClose(CLOSE_NORMAL, reason)
|
||||
|
||||
# send_* method are automatically callable through .msg(heartbeat={}) etc
|
||||
|
||||
def send_authenticate(self, *args, **kwargs):
|
||||
"""
|
||||
Send grapevine authentication. This should be send immediately upon connection.
|
||||
|
||||
"""
|
||||
data = {
|
||||
"event": "authenticate",
|
||||
"payload": {
|
||||
"client_id": GRAPEVINE_CLIENT_ID,
|
||||
"client_secret": GRAPEVINE_CLIENT_SECRET,
|
||||
"supports": ["channels"],
|
||||
"channels": GRAPEVINE_CHANNELS,
|
||||
"version": "1.0.0",
|
||||
"user_agent": get_evennia_version('pretty')
|
||||
}
|
||||
}
|
||||
# override on-the-fly
|
||||
data.update(kwargs)
|
||||
|
||||
self._send_json(data)
|
||||
|
||||
def send_heartbeat(self, *args, **kwargs):
|
||||
"""
|
||||
Send heartbeat to remote grapevine server.
|
||||
|
||||
"""
|
||||
# pass along all connected players
|
||||
data = {
|
||||
"event": "heartbeat",
|
||||
"payload": {
|
||||
}
|
||||
}
|
||||
sessions = self.sessionhandler.get_sessions(include_unloggedin=False)
|
||||
data['payload']['players'] = [sess.account.key for sess in sessions
|
||||
if hasattr(sess, "account")]
|
||||
|
||||
self._send_json(data)
|
||||
|
||||
def send_subscribe(self, channelname, *args, **kwargs):
|
||||
"""
|
||||
Subscribe to new grapevine channel
|
||||
|
||||
Use with session.msg(subscribe="channelname")
|
||||
"""
|
||||
data = {
|
||||
"event": "channels/subscribe",
|
||||
"payload": {
|
||||
"channel": channelname
|
||||
}
|
||||
}
|
||||
self._send_json(data)
|
||||
|
||||
def send_unsubscribe(self, channelname, *args, **kwargs):
|
||||
"""
|
||||
Un-subscribe to a grapevine channel
|
||||
|
||||
Use with session.msg(unsubscribe="channelname")
|
||||
"""
|
||||
data = {
|
||||
"event": "channels/unsubscribe",
|
||||
"payload": {
|
||||
"channel": channelname
|
||||
}
|
||||
}
|
||||
self._send_json(data)
|
||||
|
||||
def send_channel(self, text, channel, sender, *args, **kwargs):
|
||||
"""
|
||||
Send text type Evennia -> grapevine
|
||||
|
||||
This is the channels/send message type
|
||||
|
||||
Use with session.msg(channel=(message, channel, sender))
|
||||
|
||||
"""
|
||||
|
||||
data = {
|
||||
"event": "channels/send",
|
||||
"payload": {
|
||||
"message": text,
|
||||
"channel": channel,
|
||||
"name": sender
|
||||
}
|
||||
}
|
||||
self._send_json(data)
|
||||
|
||||
def send_default(self, *args, **kwargs):
|
||||
"""
|
||||
Ignore other outputfuncs
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def data_in(self, data, **kwargs):
|
||||
"""
|
||||
Send data grapevine -> Evennia
|
||||
|
||||
Kwargs:
|
||||
data (dict): Converted json data.
|
||||
|
||||
"""
|
||||
event = data['event']
|
||||
if event == "authenticate":
|
||||
# server replies to our auth handshake
|
||||
if data['status'] != "success":
|
||||
log_err("Grapevine authentication failed.")
|
||||
self.disconnect()
|
||||
else:
|
||||
log_info("Connected and authenticated to Grapevine network.")
|
||||
elif event == "heartbeat":
|
||||
# server sends heartbeat - we have to send one back
|
||||
self.send_heartbeat()
|
||||
elif event == "restart":
|
||||
# set the expected downtime
|
||||
self.restart_downtime = data['payload']['downtime']
|
||||
elif event == "channels/subscribe":
|
||||
# subscription verification
|
||||
if data.get('status', 'success') == "failure":
|
||||
err = data.get("error", "N/A")
|
||||
self.sessionhandler.data_in(bot_data_in=((f"Grapevine error: {err}"),
|
||||
{'event': event}))
|
||||
elif event == "channels/unsubscribe":
|
||||
# unsubscribe-verification
|
||||
pass
|
||||
elif event == "channels/broadcast":
|
||||
# incoming broadcast from network
|
||||
payload = data["payload"]
|
||||
|
||||
print("channels/broadcast:", payload['channel'], self.channel)
|
||||
if str(payload['channel']) != self.channel:
|
||||
# only echo from channels this particular bot actually listens to
|
||||
return
|
||||
else:
|
||||
# correct channel
|
||||
self.sessionhandler.data_in(
|
||||
self, bot_data_in=(
|
||||
str(payload['message'],),
|
||||
{"event": event,
|
||||
"grapevine_channel": str(payload['channel']),
|
||||
"sender": str(payload['name']),
|
||||
"game": str(payload['game'])}))
|
||||
elif event == "channels/send":
|
||||
pass
|
||||
else:
|
||||
self.sessionhandler.data_in(self, bot_data_in=("", kwargs))
|
||||
Loading…
Add table
Add a link
Reference in a new issue