strip down to base AP code
This commit is contained in:
parent
5e9b2cd275
commit
c91d0a6755
9
relay.yaml.example
Normal file
9
relay.yaml.example
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# this is the path that the object graph will get dumped to (in JSON-LD format),
|
||||||
|
# you probably shouldn't change it, but you can if you want.
|
||||||
|
db: relay.jsonld
|
||||||
|
|
||||||
|
# this section is for ActivityPub
|
||||||
|
ap:
|
||||||
|
# this is used for generating activitypub messages, as well as instructions for
|
||||||
|
# linking AP identities. it should be an SSL-enabled domain reachable by https.
|
||||||
|
host: 'relay.pleroma.site'
|
|
@ -8,7 +8,7 @@ import yaml
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
with open('viera.yaml') as f:
|
with open('relay.yaml') as f:
|
||||||
return yaml.load(f)
|
return yaml.load(f)
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,8 @@ async def actor(request):
|
||||||
"followers": "https://{}/followers".format(request.host),
|
"followers": "https://{}/followers".format(request.host),
|
||||||
"following": "https://{}/following".format(request.host),
|
"following": "https://{}/following".format(request.host),
|
||||||
"inbox": "https://{}/inbox".format(request.host),
|
"inbox": "https://{}/inbox".format(request.host),
|
||||||
"name": "Viera",
|
"sharedInbox": "https://{}/inbox".format(request.host),
|
||||||
|
"name": "ActivityRelay",
|
||||||
"type": "Application",
|
"type": "Application",
|
||||||
"id": "https://{}/actor".format(request.host),
|
"id": "https://{}/actor".format(request.host),
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
|
@ -51,8 +52,8 @@ async def actor(request):
|
||||||
"owner": "https://{}/actor".format(request.host),
|
"owner": "https://{}/actor".format(request.host),
|
||||||
"publicKeyPem": DATABASE["actorKeys"]["publicKey"]
|
"publicKeyPem": DATABASE["actorKeys"]["publicKey"]
|
||||||
},
|
},
|
||||||
"summary": "Viera, the bot",
|
"summary": "ActivityRelay bot",
|
||||||
"preferredUsername": "viera",
|
"preferredUsername": "relay",
|
||||||
"url": "https://{}/actor".format(request.host)
|
"url": "https://{}/actor".format(request.host)
|
||||||
}
|
}
|
||||||
return aiohttp.web.json_response(data)
|
return aiohttp.web.json_response(data)
|
||||||
|
@ -73,7 +74,7 @@ async def push_message_to_actor(actor, message, our_key_id):
|
||||||
'(request-target)': 'post {}'.format(url.path),
|
'(request-target)': 'post {}'.format(url.path),
|
||||||
'Content-Length': str(len(data)),
|
'Content-Length': str(len(data)),
|
||||||
'Content-Type': 'application/activity+json',
|
'Content-Type': 'application/activity+json',
|
||||||
'User-Agent': 'Viera'
|
'User-Agent': 'ActivityRelay'
|
||||||
}
|
}
|
||||||
headers['signature'] = sign_headers(headers, PRIVKEY, our_key_id)
|
headers['signature'] = sign_headers(headers, PRIVKEY, our_key_id)
|
||||||
|
|
||||||
|
@ -128,28 +129,8 @@ def strip_html(data):
|
||||||
return cgi.escape(no_tags)
|
return cgi.escape(no_tags)
|
||||||
|
|
||||||
|
|
||||||
from .authreqs import check_reqs, get_irc_bot
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_create(actor, data, request):
|
async def handle_create(actor, data, request):
|
||||||
# for now, we only care about Notes
|
pass
|
||||||
if data['object']['type'] != 'Note':
|
|
||||||
return
|
|
||||||
|
|
||||||
# strip the HTML if present
|
|
||||||
content = strip_html(data['object']['content']).split()
|
|
||||||
|
|
||||||
# check if the message is an authorization token for linking identities together
|
|
||||||
# if it is, then it's not a message we want to relay to IRC.
|
|
||||||
if check_reqs(content, actor):
|
|
||||||
return
|
|
||||||
|
|
||||||
# check that the message is public before relaying
|
|
||||||
public_uri = 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
|
|
||||||
if public_uri in data.get('to', []) or public_uri in data.get('cc', []):
|
|
||||||
bot = get_irc_bot()
|
|
||||||
bot.relay_message(actor, data['object'], ' '.join(content))
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_follow(actor, data, request):
|
async def handle_follow(actor, data, request):
|
|
@ -5,7 +5,7 @@ from . import app
|
||||||
async def webfinger(request):
|
async def webfinger(request):
|
||||||
subject = request.query['resource']
|
subject = request.query['resource']
|
||||||
|
|
||||||
if subject != 'acct:viera@{}'.format(request.host):
|
if subject != 'acct:relay@{}'.format(request.host):
|
||||||
return aiohttp.web.json_response({'error': 'user not found'}, status=404)
|
return aiohttp.web.json_response({'error': 'user not found'}, status=404)
|
||||||
|
|
||||||
actor_uri = "https://{}/actor".format(request.host)
|
actor_uri = "https://{}/actor".format(request.host)
|
|
@ -1,53 +0,0 @@
|
||||||
# this is the path that the object graph will get dumped to (in JSON-LD format),
|
|
||||||
# you probably shouldn't change it, but you can if you want.
|
|
||||||
db: viera.jsonld
|
|
||||||
|
|
||||||
# this section configures the IRC bot
|
|
||||||
irc:
|
|
||||||
# hostname of IRC network to connect to
|
|
||||||
host: chat.freenode.net
|
|
||||||
|
|
||||||
# port of IRC network to connect to
|
|
||||||
port: 6697
|
|
||||||
|
|
||||||
# whether to use SSL/TLS to connect or not
|
|
||||||
ssl: true
|
|
||||||
|
|
||||||
# the main nickname of the bot to use
|
|
||||||
nickname: viera
|
|
||||||
|
|
||||||
# the username of the bot to use
|
|
||||||
username: viera
|
|
||||||
|
|
||||||
# the realname / GECOS of the bot to use
|
|
||||||
realname: Viera; https://viera.dereferenced.org
|
|
||||||
|
|
||||||
# channels for the bot to join
|
|
||||||
channels:
|
|
||||||
- '#mychannel'
|
|
||||||
- '#myotherchannel'
|
|
||||||
|
|
||||||
# channels for the bot to relay AP posts to
|
|
||||||
relay_channels:
|
|
||||||
|
|
||||||
# allow any AP actor to be relayed
|
|
||||||
'#mychannel': []
|
|
||||||
|
|
||||||
# allow only one AP actor to be relayed
|
|
||||||
'#myotherchannel':
|
|
||||||
- 'https://example.org/~alyssa'
|
|
||||||
|
|
||||||
# IRC services credentials.
|
|
||||||
sasl_username: viera
|
|
||||||
sasl_password: examplepass
|
|
||||||
|
|
||||||
# IRC users with accounts linked to these AP identities may administer the bot
|
|
||||||
# (follow and unfollow commands)
|
|
||||||
privileged:
|
|
||||||
- 'https://pleroma.site/users/kaniini'
|
|
||||||
|
|
||||||
# this section is for ActivityPub
|
|
||||||
ap:
|
|
||||||
# this is used for generating activitypub messages, as well as instructions for
|
|
||||||
# linking AP identities. it should be an SSL-enabled domain reachable by https.
|
|
||||||
host: 'viera.dereferenced.org'
|
|
|
@ -1,72 +0,0 @@
|
||||||
import uuid
|
|
||||||
import logging
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
|
|
||||||
from .database import DATABASE
|
|
||||||
|
|
||||||
|
|
||||||
PendingAuth = namedtuple('PendingAuth', ['irc_nickname', 'irc_account', 'actor'])
|
|
||||||
|
|
||||||
|
|
||||||
AUTHS = DATABASE.get('auths', {})
|
|
||||||
DATABASE["auths"] = AUTHS
|
|
||||||
PENDING_AUTHS = {}
|
|
||||||
IRC_BOT = None
|
|
||||||
|
|
||||||
|
|
||||||
def check_reqs(chunks, actor):
|
|
||||||
global DATABASE
|
|
||||||
|
|
||||||
results = [x in PENDING_AUTHS for x in chunks]
|
|
||||||
logging.debug('AUTHREQ: chunks: %r, results: %r', chunks, results)
|
|
||||||
|
|
||||||
if True in results:
|
|
||||||
pending_slot = results.index(True)
|
|
||||||
pending_uuid = chunks[pending_slot]
|
|
||||||
req = PENDING_AUTHS.pop(pending_uuid)._replace(actor=actor["id"])
|
|
||||||
|
|
||||||
logging.debug("IRC BOT: %r, AUTHREQ: %r", IRC_BOT, req)
|
|
||||||
|
|
||||||
if IRC_BOT:
|
|
||||||
IRC_BOT.handle_auth_req(req)
|
|
||||||
|
|
||||||
DATABASE["auths"][req.irc_account] = req.actor
|
|
||||||
|
|
||||||
return True in results
|
|
||||||
|
|
||||||
|
|
||||||
def new_auth_req(irc_nickname, irc_account):
|
|
||||||
authid = str(uuid.uuid4())
|
|
||||||
PENDING_AUTHS[authid] = PendingAuth(irc_nickname, irc_account, None)
|
|
||||||
|
|
||||||
return authid
|
|
||||||
|
|
||||||
|
|
||||||
# XXX - utter hackjob
|
|
||||||
def set_irc_bot(bot):
|
|
||||||
global IRC_BOT
|
|
||||||
|
|
||||||
IRC_BOT = bot
|
|
||||||
logging.debug("SET IRC BOT TO: %r", bot)
|
|
||||||
|
|
||||||
|
|
||||||
def get_irc_bot():
|
|
||||||
global IRC_BOT
|
|
||||||
|
|
||||||
return IRC_BOT
|
|
||||||
|
|
||||||
|
|
||||||
def check_auth(account):
|
|
||||||
return account in DATABASE["auths"]
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_auth(account):
|
|
||||||
if check_auth(account):
|
|
||||||
return DATABASE["auths"][account]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def drop_auth(account):
|
|
||||||
DATABASE["auths"].pop(account, None)
|
|
277
viera/irc.py
277
viera/irc.py
|
@ -1,277 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import base64
|
|
||||||
import html
|
|
||||||
|
|
||||||
from blinker import signal
|
|
||||||
|
|
||||||
from . import CONFIG
|
|
||||||
from .actor import follow_remote_actor, unfollow_remote_actor
|
|
||||||
from .irc_envelope import RFC1459Message
|
|
||||||
|
|
||||||
from .authreqs import new_auth_req, set_irc_bot, check_auth, fetch_auth, drop_auth
|
|
||||||
IRC_CONFIG = CONFIG.get('irc', {})
|
|
||||||
AP_CONFIG = CONFIG.get('irc', {'host': 'localhost'})
|
|
||||||
|
|
||||||
# SASL_PAYLOAD = base64.b64encode(b'\x00'.join([IRC_CONFIG['sasl_username'], IRC_CONFIG['sasl_username'], IRC_CONFIG['sasl_password']]))
|
|
||||||
|
|
||||||
|
|
||||||
class IRCProtocol(asyncio.Protocol):
|
|
||||||
def connection_made(self, transport):
|
|
||||||
self.pending_actions = {}
|
|
||||||
|
|
||||||
self.caps_available = []
|
|
||||||
self.caps_requested = []
|
|
||||||
self.caps_acknowledged = []
|
|
||||||
|
|
||||||
self.transport = transport
|
|
||||||
self.recv_buffer = b''
|
|
||||||
|
|
||||||
self.send_message(verb='CAP', params=['LS'])
|
|
||||||
self.send_message(verb='NICK', params=[IRC_CONFIG['nickname']])
|
|
||||||
self.send_message(verb='USER', params=[IRC_CONFIG['username'], '*', '*', IRC_CONFIG['realname']])
|
|
||||||
|
|
||||||
def data_received(self, data):
|
|
||||||
self.recv_buffer += data
|
|
||||||
recvd = self.recv_buffer.replace(b'\r', b'').split(b'\n')
|
|
||||||
self.recv_buffer = recvd.pop(-1)
|
|
||||||
|
|
||||||
[self.line_received(m) for m in recvd]
|
|
||||||
|
|
||||||
def line_received(self, data):
|
|
||||||
data = data.decode('UTF-8', 'replace').strip('\r\n')
|
|
||||||
|
|
||||||
m = RFC1459Message.from_message(data)
|
|
||||||
self.message_received(m)
|
|
||||||
|
|
||||||
def request_caps(self, caps):
|
|
||||||
caps = [cap for cap in caps if cap in self.caps_available]
|
|
||||||
caps = [cap for cap in caps if cap not in self.caps_requested]
|
|
||||||
|
|
||||||
logging.debug('IRC: requesting caps: %r', caps)
|
|
||||||
|
|
||||||
self.caps_requested += caps
|
|
||||||
self.send_message(verb='CAP', params=['REQ', ' '.join(caps)])
|
|
||||||
|
|
||||||
def end_caps(self):
|
|
||||||
self.send_message(verb='CAP', params=['END'])
|
|
||||||
|
|
||||||
def do_blind_authenticate(self, username, password):
|
|
||||||
username = username.encode('ascii')
|
|
||||||
password = password.encode('ascii')
|
|
||||||
|
|
||||||
payload = b'\x00'.join([username, username, password])
|
|
||||||
payload = base64.b64encode(payload).decode('ascii')
|
|
||||||
|
|
||||||
self.send_message(verb='AUTHENTICATE', params=['PLAIN'])
|
|
||||||
self.send_message(verb='AUTHENTICATE', params=[payload])
|
|
||||||
|
|
||||||
def handle_cap_message(self, message):
|
|
||||||
self.cap_requested = True
|
|
||||||
if message.params[1] == 'LS':
|
|
||||||
caps = message.params[2].split()
|
|
||||||
logging.debug('IRC: available caps: %r', caps)
|
|
||||||
|
|
||||||
self.caps_available += message.params[2].split()
|
|
||||||
self.request_caps(['sasl', 'extended-join', 'account-tag', 'account-notify'])
|
|
||||||
elif message.params[1] == 'ACK':
|
|
||||||
caps = message.params[2].split()
|
|
||||||
logging.debug('IRC: acknowledged caps: %r', caps)
|
|
||||||
|
|
||||||
self.caps_acknowledged += caps
|
|
||||||
if 'sasl' in self.caps_acknowledged:
|
|
||||||
self.do_blind_authenticate(IRC_CONFIG['sasl_username'], IRC_CONFIG['sasl_password'])
|
|
||||||
else:
|
|
||||||
self.end_caps()
|
|
||||||
|
|
||||||
def join_channels(self):
|
|
||||||
self.send_message(verb='JOIN', params=[','.join(IRC_CONFIG['channels'])])
|
|
||||||
|
|
||||||
def say(self, target, message, verb='NOTICE'):
|
|
||||||
self.send_message(verb=verb, params=[target, message])
|
|
||||||
|
|
||||||
def invite(self, nickname):
|
|
||||||
[self.send_message(verb="INVITE", params=[nickname, chan]) for chan in IRC_CONFIG['channels']]
|
|
||||||
|
|
||||||
def voice(self, nickname):
|
|
||||||
[self.send_message(verb="MODE", params=[chan, "+v", nickname]) for chan in IRC_CONFIG['channels']]
|
|
||||||
|
|
||||||
def whois(self, nickname, chan, account):
|
|
||||||
data = fetch_auth(account)
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
self.say(chan, '\x02{0}\x02: \x02{1}\x02'.format(nickname, data), verb='PRIVMSG')
|
|
||||||
|
|
||||||
def follow(self, nickname, actor_uri):
|
|
||||||
asyncio.ensure_future(follow_remote_actor(actor_uri))
|
|
||||||
self.say(nickname, 'Following \x02{}\x02'.format(actor_uri))
|
|
||||||
|
|
||||||
def unfollow(self, nickname, actor_uri):
|
|
||||||
asyncio.ensure_future(unfollow_remote_actor(actor_uri))
|
|
||||||
self.say(nickname, 'Unfollowing \x02{}\x02'.format(actor_uri))
|
|
||||||
|
|
||||||
def set_pending_action(self, nickname, action):
|
|
||||||
if nickname not in self.pending_actions:
|
|
||||||
self.pending_actions[nickname] = action
|
|
||||||
|
|
||||||
def process_pending_action(self, nickname, account=None):
|
|
||||||
if nickname not in self.pending_actions:
|
|
||||||
return
|
|
||||||
action = self.pending_actions.pop(nickname)
|
|
||||||
if action == 'voice':
|
|
||||||
self.voice(nickname)
|
|
||||||
elif action == 'invite':
|
|
||||||
self.invite(nickname)
|
|
||||||
elif action == 'drop':
|
|
||||||
data = fetch_auth(account)
|
|
||||||
drop_auth(account)
|
|
||||||
self.say(nickname, "The association of \x02{0}\x02 with \x02{1}\x02 has been dropped.".format(account, data))
|
|
||||||
elif 'whois' in action:
|
|
||||||
self.whois(nickname, action['whois'], account)
|
|
||||||
elif 'follow' in action:
|
|
||||||
data = fetch_auth(account)
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
if data not in IRC_CONFIG['privileged']:
|
|
||||||
self.say(nickname, "Access denied: \x02{0}\x02 is unprivileged.".format(data))
|
|
||||||
return
|
|
||||||
logging.info('allowed follow: %r', action['follow'])
|
|
||||||
self.follow(nickname, action['follow'])
|
|
||||||
elif 'unfollow' in action:
|
|
||||||
data = fetch_auth(account)
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
if data not in IRC_CONFIG['privileged']:
|
|
||||||
self.say(nickname, "Access denied: \x02{0}\x02 is unprivileged.".format(data))
|
|
||||||
return
|
|
||||||
logging.info('allowed unfollow: %r', action['unfollow'])
|
|
||||||
self.unfollow(nickname, action['unfollow'])
|
|
||||||
|
|
||||||
def handle_auth_req(self, req):
|
|
||||||
self.say(req.irc_nickname, "The actor \x02{0}\x02 is now linked to the IRC account \x02{1}\x02.".format(req.actor, req.irc_account))
|
|
||||||
self.set_pending_action(req.irc_nickname, 'voice')
|
|
||||||
self.process_pending_action(req.irc_nickname)
|
|
||||||
|
|
||||||
def pending_whois(self, nickname, pop=False):
|
|
||||||
if nickname not in self.pending_actions:
|
|
||||||
return False
|
|
||||||
data = self.pending_actions[nickname]
|
|
||||||
if isinstance(data, dict) and 'whois' in data:
|
|
||||||
return True
|
|
||||||
if pop:
|
|
||||||
self.pending_actions.pop(nickname)
|
|
||||||
|
|
||||||
def handle_whox(self, message):
|
|
||||||
nickname = message.params[1]
|
|
||||||
account = message.params[2]
|
|
||||||
|
|
||||||
if not check_auth(account) and not self.pending_whois(nickname, True):
|
|
||||||
auth = new_auth_req(nickname, account)
|
|
||||||
self.say(nickname, "Authentication is required for this action. In order to prove your identity, you need to send me a token via the fediverse.")
|
|
||||||
self.say(nickname, "On most platforms, posting like this will work: \x02@viera@{1} {0}\x02".format(auth, AP_CONFIG['host']))
|
|
||||||
self.say(nickname, "This token is ephemeral, so you can send it to me publicly if your platform does not support direct messages.")
|
|
||||||
else:
|
|
||||||
self.process_pending_action(nickname, account)
|
|
||||||
|
|
||||||
def fetch_account_whox(self, message):
|
|
||||||
source_nick = message.source.split('!')[0]
|
|
||||||
self.send_message(verb='WHO', params=[source_nick, "%na"])
|
|
||||||
|
|
||||||
def handle_private_message(self, message):
|
|
||||||
source_nick = message.source.split('!')[0]
|
|
||||||
if message.params[1] == 'auth':
|
|
||||||
self.fetch_account_whox(message)
|
|
||||||
elif message.params[1] in ('voice', 'invite', 'drop'):
|
|
||||||
self.set_pending_action(source_nick, message.params[1])
|
|
||||||
self.fetch_account_whox(message)
|
|
||||||
elif message.params[1][0:6] == 'follow':
|
|
||||||
chunks = message.params[1].split()
|
|
||||||
logging.info('considering whether to follow: %r', chunks[1])
|
|
||||||
|
|
||||||
self.set_pending_action(source_nick, {'follow': chunks[1]})
|
|
||||||
self.fetch_account_whox(message)
|
|
||||||
elif message.params[1][0:8] == 'unfollow':
|
|
||||||
chunks = message.params[1].split()
|
|
||||||
logging.info('considering whether to unfollow: %r', chunks[1])
|
|
||||||
|
|
||||||
self.set_pending_action(source_nick, {'unfollow': chunks[1]})
|
|
||||||
self.fetch_account_whox(message)
|
|
||||||
|
|
||||||
def handle_public_message(self, message):
|
|
||||||
if not message.params[1].startswith(IRC_CONFIG['nickname']):
|
|
||||||
return
|
|
||||||
|
|
||||||
chunks = message.params[1].split()
|
|
||||||
if chunks[1] == 'whois':
|
|
||||||
self.set_pending_action(chunks[2], {'whois': message.params[0]})
|
|
||||||
self.send_message(verb='WHO', params=[chunks[2], "%na"])
|
|
||||||
|
|
||||||
def handle_chat_message(self, message):
|
|
||||||
if message.params[0] == IRC_CONFIG['nickname']:
|
|
||||||
self.handle_private_message(message)
|
|
||||||
else:
|
|
||||||
self.handle_public_message(message)
|
|
||||||
|
|
||||||
def handle_join(self, message):
|
|
||||||
source_nick = message.source.split('!')[0]
|
|
||||||
if check_auth(message.params[1]):
|
|
||||||
self.set_pending_action(source_nick, 'voice')
|
|
||||||
self.process_pending_action(source_nick, message.params[1])
|
|
||||||
|
|
||||||
def message_received(self, message):
|
|
||||||
if message.verb in ('PRIVMSG', 'NOTICE'):
|
|
||||||
self.handle_chat_message(message)
|
|
||||||
elif message.verb == '001':
|
|
||||||
self.join_channels()
|
|
||||||
elif message.verb == 'JOIN':
|
|
||||||
self.handle_join(message)
|
|
||||||
elif message.verb == 'CAP':
|
|
||||||
self.handle_cap_message(message)
|
|
||||||
elif message.verb == '354':
|
|
||||||
self.handle_whox(message)
|
|
||||||
elif message.verb == '433':
|
|
||||||
self.send_message(verb='NICK', params=[message.params[0] + '_'])
|
|
||||||
elif message.verb in ('900', '901', '902', '903', '904', '905', '906', '907'):
|
|
||||||
self.end_caps()
|
|
||||||
elif message.verb == 'PING':
|
|
||||||
self.send_message(verb='PONG', params=message.params)
|
|
||||||
elif message.verb in ('AUTHENTICATE',):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
logging.debug('IRC: unhandled inbound message: %r', message)
|
|
||||||
|
|
||||||
def send_message(self, **kwargs):
|
|
||||||
m = RFC1459Message.from_data(**kwargs)
|
|
||||||
logging.debug('> %r', m)
|
|
||||||
self.transport.write(m.to_message().encode('utf-8') + b'\r\n')
|
|
||||||
|
|
||||||
def relay_message(self, actor, obj, content):
|
|
||||||
fmt = "\x02{name}\x02: {content} [{url}]"
|
|
||||||
|
|
||||||
content = html.unescape(content)
|
|
||||||
content = html.unescape(content)
|
|
||||||
msgcontent = content[0:256]
|
|
||||||
if len(content) > 256:
|
|
||||||
msgcontent += '...'
|
|
||||||
|
|
||||||
targets = [chan for chan, actors in IRC_CONFIG['relay_channels'].items() if not actors or actor['id'] in actors]
|
|
||||||
message = fmt.format(name=actor['name'], content=msgcontent, url=obj['id'])
|
|
||||||
target = ','.join(targets)
|
|
||||||
|
|
||||||
self.say(target, message)
|
|
||||||
|
|
||||||
|
|
||||||
async def irc_bot():
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
if 'host' not in IRC_CONFIG:
|
|
||||||
return
|
|
||||||
|
|
||||||
server = IRC_CONFIG['host']
|
|
||||||
port = IRC_CONFIG['port']
|
|
||||||
ssl = IRC_CONFIG['ssl']
|
|
||||||
|
|
||||||
transport, protocol = await loop.create_connection(IRCProtocol, host=server, port=port, ssl=ssl)
|
|
||||||
set_irc_bot(protocol)
|
|
||||||
|
|
||||||
logging.info('IRC bot ready.')
|
|
|
@ -1,146 +0,0 @@
|
||||||
# irc_envelope.py
|
|
||||||
# Purpose: Conversion of RFC1459 messages to/from native objects.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2014, William Pitcock <nenolod@dereferenced.org>
|
|
||||||
#
|
|
||||||
# Permission to use, copy, modify, and/or distribute this software for any
|
|
||||||
# purpose with or without fee is hereby granted, provided that the above
|
|
||||||
# copyright notice and this permission notice appear in all copies.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
||||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
||||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
||||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
||||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
||||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
||||||
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
||||||
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
class RFC1459Message(object):
|
|
||||||
@classmethod
|
|
||||||
def from_data(cls, verb, params=None, source=None, tags=None):
|
|
||||||
o = cls()
|
|
||||||
o.verb = verb
|
|
||||||
o.tags = dict()
|
|
||||||
o.source = None
|
|
||||||
o.params = list()
|
|
||||||
|
|
||||||
if params:
|
|
||||||
o.params = params
|
|
||||||
|
|
||||||
if source:
|
|
||||||
o.source = source
|
|
||||||
|
|
||||||
if tags:
|
|
||||||
o.tags.update(**tags)
|
|
||||||
|
|
||||||
return o
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_message(cls, message):
|
|
||||||
if isinstance(message, bytes):
|
|
||||||
message = message.decode('UTF-8', 'replace')
|
|
||||||
|
|
||||||
s = message.split(' ')
|
|
||||||
|
|
||||||
tags = None
|
|
||||||
if s[0].startswith('@'):
|
|
||||||
tag_str = s[0][1:].split(';')
|
|
||||||
s = s[1:]
|
|
||||||
tags = {}
|
|
||||||
|
|
||||||
for tag in tag_str:
|
|
||||||
if '=' in tag:
|
|
||||||
k, v = tag.split('=', 1)
|
|
||||||
tags[k] = v
|
|
||||||
else:
|
|
||||||
tags[tag] = True
|
|
||||||
|
|
||||||
source = None
|
|
||||||
if s[0].startswith(':'):
|
|
||||||
source = s[0][1:]
|
|
||||||
s = s[1:]
|
|
||||||
|
|
||||||
verb = s[0].upper()
|
|
||||||
original_params = s[1:]
|
|
||||||
params = []
|
|
||||||
|
|
||||||
while len(original_params):
|
|
||||||
# skip multiple spaces in middle of message, as per 1459
|
|
||||||
if original_params[0] == '' and len(original_params) > 1:
|
|
||||||
original_params.pop(0)
|
|
||||||
continue
|
|
||||||
elif original_params[0].startswith(':'):
|
|
||||||
arg = ' '.join(original_params)[1:]
|
|
||||||
params.append(arg)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
params.append(original_params.pop(0))
|
|
||||||
|
|
||||||
return cls.from_data(verb, params, source, tags)
|
|
||||||
|
|
||||||
def args_to_message(self):
|
|
||||||
base = []
|
|
||||||
for arg in self.params:
|
|
||||||
casted = str(arg)
|
|
||||||
if casted and ' ' not in casted and casted[0] != ':':
|
|
||||||
base.append(casted)
|
|
||||||
else:
|
|
||||||
base.append(':' + casted)
|
|
||||||
break
|
|
||||||
|
|
||||||
return ' '.join(base)
|
|
||||||
|
|
||||||
def to_message(self):
|
|
||||||
components = []
|
|
||||||
|
|
||||||
if self.tags:
|
|
||||||
components.append('@' + ';'.join([k + '=' + v for k, v in self.tags.items()]))
|
|
||||||
|
|
||||||
if self.source:
|
|
||||||
components.append(':' + self.source)
|
|
||||||
|
|
||||||
components.append(self.verb)
|
|
||||||
|
|
||||||
if self.params:
|
|
||||||
components.append(self.args_to_message())
|
|
||||||
|
|
||||||
return ' '.join(components)
|
|
||||||
|
|
||||||
def to_event(self):
|
|
||||||
return "rfc1459 message " + self.verb, self.__dict__
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return self.__dict__
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '[RFC1459Message: "{0}"]'.format(self.to_message())
|
|
||||||
|
|
||||||
def test_rfc1459message():
|
|
||||||
print('====== PARSER TESTS ======')
|
|
||||||
print(RFC1459Message.from_message('@foo=bar PRIVMSG kaniini :this is a test message!'))
|
|
||||||
print(RFC1459Message.from_message('@foo=bar :irc.tortois.es 001 kaniini :Welcome to IRC, kaniini!'))
|
|
||||||
print(RFC1459Message.from_message('PRIVMSG kaniini :this is a test message!'))
|
|
||||||
print(RFC1459Message.from_message(':irc.tortois.es 001 kaniini :Welcome to IRC, kaniini!'))
|
|
||||||
print(RFC1459Message.from_message('CAPAB '))
|
|
||||||
|
|
||||||
print('====== STRUCTURE TESTS ======')
|
|
||||||
m = RFC1459Message.from_message('@foo=bar;bar=baz :irc.tortois.es 001 kaniini :Welcome to IRC, kaniini!')
|
|
||||||
pprint(m.serialize())
|
|
||||||
|
|
||||||
print('====== BUILDER TESTS ======')
|
|
||||||
data = {
|
|
||||||
'verb': 'PRIVMSG',
|
|
||||||
'params': ['kaniini', 'hello world!'],
|
|
||||||
'source': 'kaniini!~kaniini@localhost',
|
|
||||||
'tags': {'account-name': 'kaniini'},
|
|
||||||
}
|
|
||||||
m = RFC1459Message.from_data(**data)
|
|
||||||
print(m.to_message())
|
|
||||||
pprint(m.serialize())
|
|
||||||
|
|
||||||
print('====== ALL TESTS: PASSED ======')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test_rfc1459message()
|
|
Loading…
Reference in a new issue