diff --git a/.gitignore b/.gitignore
index 9631938..23b1b1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
/fishroom/config.py
/test/config.py
config.py.production
+**/__pycache__
+**/*.py[cod]
diff --git a/fishroom/DiscourseBabble.py b/fishroom/DiscourseBabble.py
new file mode 100644
index 0000000..26865d6
--- /dev/null
+++ b/fishroom/DiscourseBabble.py
@@ -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'',
+ 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]
{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" % (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
diff --git a/fishroom/config.py.example b/fishroom/config.py.example
index 1c19131..aab06e3 100644
--- a/fishroom/config.py.example
+++ b/fishroom/config.py.example
@@ -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": {},
diff --git a/fishroom/models.py b/fishroom/models.py
index d838baa..c59dab9 100755
--- a/fishroom/models.py
+++ b/fishroom/models.py
@@ -16,6 +16,7 @@ class ChannelType(object):
Wechat = "wechat"
Web = "web"
API = "api"
+ DiscourseBabble = "babble"
class MessageType(object):
diff --git a/fishroom/photostore.py b/fishroom/photostore.py
index df1f582..6464db6 100644
--- a/fishroom/photostore.py
+++ b/fishroom/photostore.py
@@ -5,6 +5,7 @@
import requests.exceptions
from base64 import b64encode
from .helpers import get_logger
+import shutil,os
logger = get_logger(__name__)
@@ -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):
@@ -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:
diff --git a/fishroom/telegram.py b/fishroom/telegram.py
index 6727f79..b2462e0 100644
--- a/fishroom/telegram.py
+++ b/fishroom/telegram.py
@@ -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 "[{sender}] {content}"
+ return "{content}" if sender is None else "{sender}\n{content}"
@classmethod
def formatRichText(cls, rich_text: RichText, escape=True):
@@ -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)
diff --git a/fishroom/wechat.py b/fishroom/wechat.py
index d160973..8c562f3 100644
--- a/fishroom/wechat.py
+++ b/fishroom/wechat.py
@@ -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)
diff --git a/fishroom/xss.py b/fishroom/xss.py
new file mode 100644
index 0000000..155bd3f
--- /dev/null
+++ b/fishroom/xss.py
@@ -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'<'],
+ [r'\>', r'>'],
+ [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))