Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/fishroom/config.py
/test/config.py
config.py.production
**/__pycache__
**/*.py[cod]
254 changes: 254 additions & 0 deletions fishroom/DiscourseBabble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
Support for the Discourse Babble plugin:
https://github.com/gdpelican/babble
"""
import requests
import requests.exceptions
import tornado
import tornado.web

import time
import re

from . import xss

from .base import BaseBotInstance, EmptyBot
from .bus import MessageBus, MsgDirection
from .models import (
Message, ChannelType, MessageType, RichText, TextStyle, Color
)
from .textformat import TextFormatter
from .helpers import get_now_date_time, get_logger
from .config import config

logger = get_logger("DiscourseBabble")

IRC_COLOR_RGB = [
'#ffffff', # 0
'#000000', # 1
'#00007f',
'#009300',
'#ff0000',
'#7f0000',
'#9c009c',
'#fc7f00',
'#ffff00',
'#00fc00',
'#009300',
'#00ffff',
'#0000fc',
'#ff00ff',
'#7f7f7f',
'#d2d2d2', #15
'#888', # 16
]


def getWebhookHandler(dbh):
class WebhookHandler(tornado.web.RequestHandler):
def post(self):
"""
Process the JSON post object.
"""
json = tornado.escape.json_decode(self.request.body)
post = json.get('post', None)
if post:
# new api
topic_id = post.get('id', 0)
current_user = post.get('username', '未知用户')
# get bbcode/markdown hybrid:
# more consice than html, still allows for image links
message = post.get('raw', '未知消息')
dbh.on_sendmessage(topic_id, current_user, message)
else:
topic_id = json.get('topic_id', 0)
current_user = json.get('current_user', '未知用户')
# what kind of data is this?
message = json.get('message', '未知消息')
dbh.on_sendmessage(topic_id, current_user, message)
self.write('Got a message.')
return WebhookHandler

class DiscourseBabbleHandle(BaseBotInstance):
ChanTag = ChannelType.DiscourseBabble
SupportMultiline = True
send_to_bus = None
def __init__(self, base_url, username, api_key, topic_ids):
debug=config['debug']
self.username = username
self.base_url = base_url
self.api_key = api_key
self.topic_ids = topic_ids
application = tornado.web.Application([
(r"/sendmessage", getWebhookHandler(self)),
],debug=debug, autoreload=debug)
application.listen(config['babble']['webhook_port'], address=config['babble'].get('webhook_host', '0.0.0.0'))

def listen(self):
tornado.ioloop.IOLoop.instance().start()

def on_sendmessage(self, topic_id, current_user, message):
if current_user == self.username:
return
date, time = get_now_date_time()
logger.debug(message)
mtype = MessageType.Text
# Replace discourse-relative media with absolute links
message = re.sub(r'!\[.*\]\(/uploads/.+\)',
r'![\1](' + self.base_url + r'/uploads/\2)',
message)
msg = Message(
ChannelType.DiscourseBabble,
current_user, topic_id,
xss.cooked_unescape(message),
mtype=mtype, media_url='',
date=date, time=time
)
self.send_to_bus(self,msg)

def do_send_request(self, topic_id, text):
if topic_id not in self.topic_ids:
return
requests.post(self.base_url
+ '/babble/topics/'
+ '%d' % int(topic_id)
+ '/posts',
data = {
'api_user': self.username,
'api_key': self.api_key,
'raw': text,
'topic_id': '%d'%int(topic_id)
})

def send_msg(self, target, content, sender=None, first=False, raw=None, **kwargs):
# --- Pick a color from username
# color that fits both dark and light background
color_avail = (2, 3, 4, 5, 6, 7, 10, 12, 13)
color = None

if sender:
# color defined at http://www.mirc.com/colors.html
# background_num = sum([ord(i) for i in sender]) % 16
cidx = sum([ord(i) for i in sender]) % len(color_avail)
foreground_num = color_avail[cidx]
color = Color(foreground_num)

reply_quote = ""
if 'reply_text' in kwargs:
reply_to = kwargs['reply_to']
reply_text = kwargs['reply_text']
if len(reply_text) > 8:
reply_text = reply_text[:8] + '...'
reply_text = reply_text.strip()
reply_quote = "[b]{reply_to}[/b]<br/>{reply_text}".format(locals())

try:
channel = raw.channel.capitalize()
channel = channel.replace('Babble', config['babble'].get('site_name', 'Discourse'))
except AttributeError:
channel = None
msg = self.rich_message(content, sender=sender, color=color,
reply_quote=reply_quote, channel=channel)
msg = self.formatRichText(msg)
if raw is not None:
if raw.mtype in (MessageType.Photo, MessageType.Sticker):
msg += "\n![](%s)" % (raw.media_url,)
self.do_send_request(target, msg)
time.sleep(0.5)

def rich_message(self, content, sender=None, color=None, reply_quote="", channel=""):
if color and sender:
return RichText([
(TextStyle(color=color, bold=1), "{}".format(sender)),
(TextStyle(color=Color(16)), " {} 用户\n".format(channel)),
(TextStyle(color=Color(16)), "{}\n".format(reply_quote)),
(TextStyle(), "{}".format(xss.md_escape(content))),
])
else:
tmpl = "{content}" if sender is None else "[{sender}] {content}"
return RichText([
(TextStyle(), tmpl.format(content=content, sender=sender))
])

def formatRichText(self, rich_text: RichText):
formated_text = ""
for ts, text in rich_text:
if not text:
continue
if ts.is_normal():
formated_text += text
continue
def bold(text):
if not ts.is_bold():
return text
return "[b]{}[/b]".format(text)
def italic(text):
if not ts.is_italic():
return text
return "[i]{}[/i]".format(text)
def underline(text):
if not ts.is_underline():
return text
return "[u]{}[/u]".format(text)
def fgcolor(text, color):
return '[color={}]{}[/color]'.format(IRC_COLOR_RGB[color], text)
def bgcolor(text, color):
return '[bgcolor={}]{}[/bgcolor]'.format(IRC_COLOR_RGB[color], text)
def color(text):
if not ts.has_color():
return text
if ts.color.bg:
return bgcolor(fgcolor(text,ts.color.fg), ts.color.bg)
else:
return fgcolor(text, ts.color.fg)
formated_text += (underline(italic(bold(color(text)))))
return formated_text

def Babble2FishroomThread(disbbl_handle: DiscourseBabbleHandle, bus: MessageBus):
if disbbl_handle is None or isinstance(disbbl_handle, EmptyBot):
return
def send_to_bus(self, msg):
logger.debug(msg)
bus.publish(msg)
disbbl_handle.send_to_bus=send_to_bus
disbbl_handle.listen()


def Fishroom2BabbleThread(disbbl_handle, bus):
if disbbl_handle is None or isinstance(disbbl_handle, EmptyBot):
return
for msg in bus.message_stream():
disbbl_handle.forward_msg_from_fishroom(msg)

def init():
from .db import get_redis
redis_client = get_redis()
im2fish_bus = MessageBus(redis_client, MsgDirection.im2fish)
fish2im_bus = MessageBus(redis_client, MsgDirection.fish2im)

babble_idnumbers = [b["babble"] for _, b in config['bindings'].items() if "babble" in b]
base_url = config['babble']['base_url']
username = config['babble']['username']
api_key = config['babble']['api_key']

return (
DiscourseBabbleHandle(base_url, username, api_key, babble_idnumbers),
im2fish_bus, fish2im_bus,
)


def main():
if "babble" not in config:
logger.error("Babble config not found in config.py! exiting...")
return

from .runner import run_threads
bot, im2fish_bus, fish2im_bus = init()
run_threads([
(Babble2FishroomThread, (bot, im2fish_bus, ), ),
(Fishroom2BabbleThread, (bot, fish2im_bus, ), ),
])

main()
# vim: ts=4 sw=4 sts=4 expandtab
8 changes: 8 additions & 0 deletions fishroom/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ config = {
# "bot_msg_pattern": "^mubot|^!wikipedia",
# },

# Uncomment these if you want discourse babble access
# "babble": {
# "username": "fishroom",
# "api_key": "",
# "base_url": "https://forum.foo.moe/",
# "webhook_port": 2334
# },

# Uncomment these if you want WeChat access
# "wechat": {},

Expand Down
1 change: 1 addition & 0 deletions fishroom/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ChannelType(object):
Wechat = "wechat"
Web = "web"
API = "api"
DiscourseBabble = "babble"


class MessageType(object):
Expand Down
14 changes: 10 additions & 4 deletions fishroom/photostore.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import requests.exceptions
from base64 import b64encode
from .helpers import get_logger
import shutil,os


logger = get_logger(__name__)
Expand All @@ -15,6 +16,13 @@ class BasePhotoStore(object):
def upload_image(self, filename, **kwargs):
raise Exception("Not Implemented")

class LocalPhotoStore(object):
def __init__(self, path, **kwargs):
self.path = path

def upload_image(self, filename=None, filedata=None, **kwargs):
if filedata is None:
shutil.copy2(filename, os.path.join(path, os.path.dirname(filename)))

class Imgur(BasePhotoStore):

Expand Down Expand Up @@ -64,10 +72,8 @@ def upload_image(self, filename=None, filedata=None, **kwargs):

class VimCN(BasePhotoStore):

url = "https://img.vim-cn.com/"

def __init__(self, **kwargs):
pass
def __init__(self, url="https://img.vim-cn.com/", **kwargs):
self.url = url

def upload_image(self, filename=None, filedata=None, **kwargs) -> str:
if filedata is None:
Expand Down
4 changes: 2 additions & 2 deletions fishroom/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ def send_msg(self, peer, content, sender=None, escape=True, rich_text=None,
self._must_post(api, json=data)

def msg_tmpl(self, sender=None):
return "{content}" if sender is None else "<b>[{sender}]</b> {content}"
return "{content}" if sender is None else "<b>{sender}</b>\n{content}"

@classmethod
def formatRichText(cls, rich_text: RichText, escape=True):
Expand Down Expand Up @@ -712,7 +712,7 @@ def photo_store_init():
options = config['photo_store']['options']
return Imgur(**options)
elif provider == "vim-cn":
return VimCN()
return VimCN(**config['photo_store']['options'])
elif provider == "qiniu":
return get_qiniu(redis_client, config)

Expand Down
2 changes: 1 addition & 1 deletion fishroom/wechat.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def init():
options = config['photo_store']['options']
photo_store = Imgur(**options)
elif provider == "vim-cn":
photo_store = VimCN()
photo_store = VimCN(**config['photo_store']['options'])
elif provider == "qiniu":
photo_store = get_qiniu(redis_client, config)

Expand Down
27 changes: 27 additions & 0 deletions fishroom/xss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#coding=utf-8
import re
import html

def md_escape(text):
replacements = [
[r'\*', r'\\*'],
[r'#', r'\\#'],
[r'\/', r'\\/'],
[r'\(', r'\\('],
[r'\)', r'\\)'],
[r'\[', r'\\['],
[r'\]', r'\\]'],
[r'\<', r'&lt;'],
[r'\>', r'&gt;'],
[r'_', r'\\_'],
[r'`', r'\\`'],
]
return replace(text, replacements)

def replace(text,rs):
for r in rs:
text = re.sub(r[0], r[1], text)
return text

def cooked_unescape(message):
return html.unescape(re.sub(r'<[^>]+>','', message))