Compare commits

...

No commits in common. "v1.02" and "master" have entirely different histories.

10 changed files with 467 additions and 221 deletions

View File

@ -1,6 +1,6 @@
Copyright (c) 2014 cxindex Copyright (c) 2014 cxindex
Copyright (c) 2015 Alexey Kharlamov <derlafff@ya.ru> Copyright (c) 2015 Alexey Kharlamov <derlafff@ya.ru>
Copyright (c) 2016 Alexei Sorokin <sor.alexei@meowr.ru> Copyright (c) 2017 Alexei Sorokin <sor.alexei@meowr.ru>
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the ""Software""), to deal in this software and associated documentation files (the ""Software""), to deal in

View File

@ -1,5 +1,5 @@
# hptoad # hptoad
An MIT licensed XMPP bot written using Python and sleekxmpp. An MIT licensed XMPP bot written using Python 3 and slixmpp.
Original project: https://gitlab.com/XRevan86/hptoad Original project: https://gitlab.com/XRevan86/hptoad

View File

@ -1,11 +0,0 @@
#!/bin/sh
user="$1" # User nickname.
admin="$2" # If one is an admin.
if [ "$admin" != 'true' ]; then
printf "%s\n" "$user: you are not an admin"
else
printf "%s\n" "$user: you are an admin"
fi

View File

@ -0,0 +1,12 @@
# -*- python -*-
class Plugin:
def command(self, command, body, nick, from_id, is_admin):
if command != "example":
return {"handled": False}
if is_admin:
reply = "%s: you are an admin" % nick
else:
reply = "%s: you are not an admin" % nick
return {"handled": True, "reply": reply}

View File

@ -0,0 +1,62 @@
# -*- python -*-
import asyncio
import os
import re
class Plugin:
_trim_regexp = re.compile("(`|\\$|\\.\\.)")
_quote_regexp = re.compile("(\"|')")
@classmethod
def _trim(cls, s):
result = cls._trim_regexp.sub("", s)
result = cls._quote_regexp.sub("", result).strip()
return result
# letter(ASCII or cyrillic), number, underscore only.
_cmd_validator_regexp = re.compile("^(\\w|\\p{Cyrillic})*$")
@asyncio.coroutine
def _exec_cmd(self, cmd, body, nick, dir_path, is_admin):
is_admin = "true" if is_admin else "false"
path = os.path.join(dir_path, self._trim(cmd))
if not self._cmd_validator_regexp.match(cmd) or \
not os.access(path, os.F_OK | os.X_OK) or not os.path.isfile(path):
return {"handled": False}
if not os.access(path, os.R_OK):
return {"handled": True,
"error": "\"%s\" is not readable" % path}
cmd = [path, self._trim(nick), is_admin, self._trim(body)]
try:
pipe = asyncio.subprocess.PIPE
proc = yield from asyncio.create_subprocess_exec(*cmd,
stdout=pipe,
stderr=pipe)
cmd_reply, cmd_error = yield from proc.communicate()
except OSError as e:
return {"handled": True,
"error": "Execute: %s" % str(e)}
result = {}
if cmd_error and len(cmd_error.strip()) > 0:
result["error"] = "Process: %s" % cmd_error.strip()
if cmd_reply and len(cmd_reply.strip()) > 0:
result["reply"] = cmd_reply.decode().strip()
if result:
result["handled"] = True
return result
@asyncio.coroutine
def command(self, command, body, nick, from_id, is_admin):
result = yield from self._exec_cmd(command, body, nick,
"plugins", is_admin)
return result
@asyncio.coroutine
def question(self, body, nick, from_id, is_admin):
result = yield from self._exec_cmd("answer", body, nick,
"chat", is_admin)
return result

View File

@ -1,8 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys import sys
import sleekxmpp import slixmpp
opts = { opts = {
"jid": "botname@example.com", "jid": "botname@example.com",
@ -12,9 +11,14 @@ opts = {
} }
def on_failed_all_auth(event):
print("Auth: Could not connect to the server, or password mismatch!")
sys.exit(1)
def on_session_start(event): def on_session_start(event):
client.get_roster()
# client.send_presence() # client.send_presence()
client.get_roster()
body = "\n".join(sys.argv[1:]).strip() body = "\n".join(sys.argv[1:]).strip()
try: try:
@ -23,19 +27,19 @@ def on_session_start(event):
except Exception as e: except Exception as e:
print("%s: %s" % (type(e).__name__, str(e))) print("%s: %s" % (type(e).__name__, str(e)))
finally: finally:
client.disconnect(wait=True) client.disconnect()
def on_session_end(event):
sys.exit(0) sys.exit(0)
if __name__ == "__main__": if __name__ == "__main__":
if sys.version_info.major < 3:
sleekxmpp.util.misc_ops.setdefaultencoding("utf-8")
if len(sys.argv) <= 1: if len(sys.argv) <= 1:
print("At least one argument is required.") print("At least one argument is required.")
sys.exit(1) sys.exit(1)
client = sleekxmpp.ClientXMPP("%s/%s" % (opts["jid"], opts["resource"]), client = slixmpp.ClientXMPP("%s/%s" % (opts["jid"], opts["resource"]),
opts["password"]) opts["password"])
if opts["connect"]: if opts["connect"]:
@ -47,9 +51,8 @@ if __name__ == "__main__":
else: else:
connect = () connect = ()
if client.connect(connect): client.connect(connect)
client.add_event_handler("failed_all_auth", on_failed_all_auth)
client.add_event_handler("session_start", on_session_start) client.add_event_handler("session_start", on_session_start)
client.process(block=True) client.add_event_handler("session_end", on_session_end)
else: client.process(forever=True)
print("Could not connect to server, or password mismatch!")
sys.exit(1)

435
hptoad.py
View File

@ -1,15 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- import asyncio
import functools
import importlib.util
import logging import logging
import os import os
import re import re
import signal import signal
import subprocess
import sys import sys
import time import types
import sleekxmpp import slixmpp
from grab import Grab # page's head title parser
opts = { opts = {
"muc": "room@conference.example.com", "muc": "room@conference.example.com",
@ -21,44 +20,129 @@ opts = {
} }
class Hptoad: class HptoadPlugin:
def __init__(self, opts): name = None
if sys.version_info.major < 3:
sleekxmpp.util.misc_ops.setdefaultencoding("utf-8")
self.client = sleekxmpp.ClientXMPP("%s/%s" % (opts["jid"], def __init__(self, name):
spec = importlib.util.find_spec("plugins.%s" % name)
module = types.ModuleType(spec.name)
spec.loader.exec_module(module)
self.name = "plugins.%s" % name
self._obj = module.Plugin()
# Calls a function in the plugin, returns a dict with three variables.
# If the called function returned something else, all extras will be
# discarded.
# "handled" must be set to True if the plugin did something with the given
# information, and if the call did nothing, then False.
# "reply" is a string with the answer and "error" is a string with an error
# information or an empty string.
@asyncio.coroutine
def _call(self, cb_name, *args, **kwargs):
ret = {}
try:
if hasattr(self._obj, cb_name):
func = getattr(self._obj, cb_name)
if asyncio.iscoroutinefunction(func):
ret = yield from func(*args, **kwargs)
else:
ret = func(*args, **kwargs)
if not ret or type(ret) != dict:
ret = {}
except Exception as e:
ret = {"error": "%s: %s" % (type(e).__name__, str(e))}
return {"handled": bool(ret.get("handled", False)),
"reply": str(ret.get("reply", "")),
"error": str(ret.get("error", ""))}
@asyncio.coroutine
def call_initiate(self):
result = yield from self._call("initiate")
return result
@asyncio.coroutine
def call_question(self, body, nick, from_id, is_admin):
result = yield from self._call("question", body, nick,
from_id, is_admin)
return result
@asyncio.coroutine
def call_command(self, command, body, nick, from_id, is_admin):
result = yield from self._call("command", command, body, nick,
from_id, is_admin)
return result
@asyncio.coroutine
def call_chat_message(self, body, nick, from_id, is_admin):
result = yield from self._call("chat_message", body, nick,
from_id, is_admin)
return result
class Hptoad:
plugins = {}
def __init__(self, opts, timeout=5.0):
self.client = slixmpp.ClientXMPP("%s/%s" % (opts["jid"],
opts["resource"]), opts["resource"]),
opts["password"]) opts["password"])
self.client.register_plugin("xep_0199") # XMPP Ping. self.client.register_plugin("xep_0199") # XMPP Ping.
self.client.register_plugin("xep_0045") # XMPP MUC. self.client.register_plugin("xep_0045") # XMPP MUC.
self.muc_obj = self.client.plugin["xep_0045"] self.muc_obj = self.client.plugin["xep_0045"]
self.muc_is_joined = False
self.timeout = timeout
self.logger = logging.getLogger(self.__class__.__name__)
self.logger.addHandler(logging.NullHandler())
self.logger.setLevel(logging.DEBUG)
self.jid = opts["jid"] self.jid = opts["jid"]
self.connect = opts["connect"] self.connect_host = opts["connect"]
self.muc = opts["muc"] self.muc = opts["muc"]
self.pure_bot_nick = opts["nick"] self.pure_bot_nick = opts["nick"]
self.bot_nick = self.pure_bot_nick self.bot_nick = self.pure_bot_nick
def register_handlers(self): def register_handlers(self):
self.client.add_event_handler("failed_all_auth",
self.on_failed_all_auth)
self.client.add_event_handler("session_start", self.on_session_start) self.client.add_event_handler("session_start", self.on_session_start)
self.client.add_event_handler("message", self.on_message, self.client.add_event_handler("got_online", self.on_got_online)
threaded=True) self.client.add_event_handler("disconnected", self.on_disconnected)
self.client.add_event_handler("message", self.on_message)
self.client.add_event_handler("muc::%s::presence" % self.muc, self.client.add_event_handler("muc::%s::presence" % self.muc,
self.on_muc_presence) self.on_muc_presence)
def connect(self):
# Reset the nick.
self.bot_nick = self.pure_bot_nick
if self.connect_host:
connect = self.connect_host.split(":", 1)
if len(connect) != 2 or not connect[1].isdigit():
self.logger.critical("Conn: Connection server format is " +
"invalid, should be example.org:5222")
sys.exit(1)
else:
connect = ()
self.client.connect(connect)
@asyncio.coroutine
def join_muc_loop(self):
while not self.muc_is_joined:
self.muc_obj.join_muc(self.muc, self.bot_nick)
yield from asyncio.sleep(self.timeout)
def join_muc(self): def join_muc(self):
if self.muc in self.muc_obj.getJoinedRooms(): asyncio.async(self.join_muc_loop())
self.muc_obj.leaveMUC(self.muc, self.bot_nick,
msg="Replaced by new connection")
self.muc_obj.joinMUC(self.muc, self.bot_nick, wait=True)
@classmethod def log_exception(self, ex):
def log_exception(cls, ex): self.logger.error("%s: %s" % (type(ex).__name__, str(ex)))
logging.error("%s: %s" % (type(ex).__name__, str(ex)))
@classmethod def log_message_event(self, event):
def log_message_event(cls, event): self.logger.debug("&{{jabber:client message} %s %s %s %s %s { }}" %
logging.debug("&{{jabber:client message} %s %s %s %s %s { }}" %
(event["from"], event["id"], event["to"], (event["from"], event["id"], event["to"],
event["type"], event["body"])) event["type"], event["body"]))
@ -66,165 +150,123 @@ class Hptoad:
if nick not in self.muc_obj.rooms[self.muc]: if nick not in self.muc_obj.rooms[self.muc]:
return False return False
affiliation = self.muc_obj.getJidProperty(muc, nick, "affiliation") affiliation = self.muc_obj.get_jid_property(muc, nick, "affiliation")
return True if affiliation in ("admin", "owner") else False return True if affiliation in ("admin", "owner") else False
_trim_regexp = re.compile("(`|\\$|\\.\\.)") @asyncio.coroutine
_quote_regexp = re.compile("(\"|')") def handle_command(self, command, body, nick, from_id, is_admin):
if command == "megakick": # Megakick.
@classmethod reply = None
def trim(cls, s): victim = body
result = cls._trim_regexp.sub("", s) if victim:
result = cls._quote_regexp.sub("", result).strip()
return result
# letter(ASCII or cyrillic), number, underscore only.
_cmd_validator_regexp = re.compile("^!(\\w|\\p{Cyrillic})*$")
def prep_extern_cmd(self, body, nick, dir_path, is_admin=False):
cmd = body.split(" ", 1)
cmd[0] = cmd[0].strip()
is_admin = "true" if is_admin else "false"
if not self._cmd_validator_regexp.match(cmd[0]):
return None, "Bad command \"%s\"" % cmd[0]
path = os.path.join(dir_path, self.trim(cmd[0][1:]))
if not os.access(path, os.F_OK):
return None, "\"%s\" does not exist" % path
if not os.path.isfile(path):
return None, "\"%s\" is not a file" % path
if not os.access(path, os.R_OK | os.X_OK):
return None, "\"%s\" is not readable or executable" % path
proc_args = [path, self.trim(nick), is_admin]
if len(cmd) > 1:
proc_args.append(self.trim(cmd[1]))
return proc_args, None
def extern_cmd(self, body, nick, from_id, dir_path, is_admin=False):
reply = ""
err = None
cmd, prep_err = self.prep_extern_cmd(body, nick, dir_path,
is_admin=is_admin)
if prep_err:
reply = "%s: WAT" % nick
err = "Command: %s" % prep_err
return reply, err
try:
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
cmd_reply, cmd_err = proc.communicate()
except subprocess.CalledProcessError as e:
reply = "%s: WAT" % nick
err = "Execute: %s" % str(e)
return reply, err
if cmd_err and len(cmd_err.strip()) > 0:
err = "Process: %s" % cmd_err.strip()
if cmd_reply and len(cmd_reply.strip()) > 0:
reply = cmd_reply.strip()
return reply, err
def handle_cmd(self, body, nick, from_id, is_admin=False):
reply = ""
err = None
if body == "!megakick": # Incomplete megakick.
reply = "%s: WAT" % nick
elif body.startswith("!megakick "): # Megakick.
victim = body.split("!megakick ", 1)[1]
is_bot_admin = self.is_muc_admin(self.muc, self.bot_nick) is_bot_admin = self.is_muc_admin(self.muc, self.bot_nick)
is_victim_admin = self.is_muc_admin(self.muc, victim) is_victim_admin = self.is_muc_admin(self.muc, victim)
if is_admin and victim != self.bot_nick: if is_admin and victim != self.bot_nick:
if is_bot_admin and not is_victim_admin and \ if is_bot_admin and not is_victim_admin and \
victim in self.muc_obj.rooms[self.muc]: victim in self.muc_obj.rooms[self.muc]:
self.muc_obj.setRole(self.muc, victim, "none") self.muc_obj.set_role(self.muc, victim, "none")
else: else:
reply = "%s: Can't megakick %s." % (nick, victim) reply = "%s: Can't megakick %s." % (nick, victim)
else: else:
reply = "%s: GTFO" % nick reply = "%s: GTFO" % nick
elif body.startswith("!"): # Any external command.
reply, err = self.extern_cmd(body, nick, from_id, "plugins",
is_admin=is_admin)
return reply, err
def handle_self_message(self, body, nick, from_id):
if body.startswith("!"):
msg, err = self.handle_cmd(body, nick, from_id, is_admin=True)
else: else:
msg = body.strip() reply = "%s: WAT" % nick
if msg and len(msg) > 0:
self.client.send_message(mto=self.muc, mbody=msg,
mtype="groupchat")
def handle_muc_message(self, body, nick, from_id):
is_admin = self.is_muc_admin(self.muc, nick)
reply = ""
err = None
# --- page's head title parser
if not (body.startswith("Link:") or body.startswith("\nLink:")) and not (body.startswith(self.bot_nick)):
links = re.findall(r'(http[s]?://\S*)',body)
if links:
for link in links:
link = link.replace('>','')
try:
g = Grab()
g.go(link)
title = g.xpath_text('//title')
except:
title = ""
if (title):
if (len(links) > 1):
reply = reply + "\nLink: %s" % title
else:
reply = reply + "Link: %s" % title
# --- page's head title parser
# Has to be redone with the current bot nick.
call_regexp = re.compile("^%s[:,]" % self.bot_nick)
if body.startswith("!"): # Any external command.
reply, err = self.handle_cmd(body, nick, from_id,
is_admin=is_admin)
elif call_regexp.match(body): # Chat.
cmd_body = call_regexp.sub("!answer", body)
reply, err = self.extern_cmd(cmd_body, nick, from_id, "chat",
is_admin=is_admin)
if err:
logging.error(err)
if is_admin:
self.client.send_message(mto=from_id, mbody=err, mtype="chat")
if reply: if reply:
self.client.send_message(mto=self.muc, mbody=reply, self.client.send_message(mto=self.muc, mbody=reply,
mtype="groupchat") mtype="groupchat")
else: # Any plugin command.
futures = []
for plugin in self.plugins.values():
future = asyncio.async(plugin.call_command(command, body, nick,
from_id, is_admin))
callback = functools.partial(self.on_plugin_got_result,
nick=nick, from_id=from_id,
is_admin=is_admin)
future.add_done_callback(callback)
futures.append(future)
if futures:
results = yield from asyncio.gather(*futures)
if not [i["handled"] for i in results if i["handled"]]:
self.client.send_message(mto=self.muc,
mbody="%s: WAT" % nick,
mtype="groupchat")
@asyncio.coroutine
def handle_self_message(self, body, nick, from_id):
if body.startswith("!"):
split = body.split(" ", 1)
command = split[0].strip()[1:]
message = split[1] if len(split) > 1 else ""
yield from self.handle_command(command, message, nick,
from_id, True)
elif body and len(body) > 0:
self.client.send_message(mto=self.muc, mbody=body.strip(),
mtype="groupchat")
@asyncio.coroutine
def handle_muc_message(self, body, nick, from_id):
is_admin = self.is_muc_admin(self.muc, nick)
futures = []
# Has to be redone with the current bot nick.
call_regexp = re.compile("^%s[:,]" % re.escape(self.bot_nick))
for plugin in self.plugins.values():
future = asyncio.async(plugin.call_chat_message(body, nick,
from_id, is_admin))
future.add_done_callback(self.on_plugin_got_result)
futures.append(future)
if body.startswith("!"): # Any plugin command.
split = body.split(" ", 1)
command = split[0].strip()[1:]
message = split[1] if len(split) > 1 else ""
yield from self.handle_command(command, message, nick, from_id,
is_admin=is_admin)
elif call_regexp.match(body): # Chat.
message = call_regexp.sub("", body).lstrip()
for plugin in self.plugins.values():
future = asyncio.async(plugin.call_question(message, nick,
from_id, is_admin))
callback = functools.partial(self.on_plugin_got_result,
nick=nick, from_id=from_id,
is_admin=is_admin)
future.add_done_callback(callback)
futures.append(future)
if futures:
yield from asyncio.gather(*futures)
def on_failed_all_auth(self, event):
self.logger.critical("Auth: Could not connect to the server, or " +
"password mismatch!")
sys.exit(1)
def on_session_start(self, event): def on_session_start(self, event):
self.client.get_roster() self.client.get_roster()
self.client.send_presence(pstatus="is there some food in this world?", self.client.send_presence(pstatus="is there some food in this world?",
ppriority=12) ppriority=12)
def on_got_online(self, event):
self.join_muc() self.join_muc()
@asyncio.coroutine
def on_disconnected(self, event):
self.muc_is_joined = False
self.logger.error("Conn: Connection lost, reattempting in %d seconds" %
self.timeout)
yield from asyncio.sleep(self.timeout)
self.connect()
@asyncio.coroutine
def on_message(self, event): def on_message(self, event):
try: try:
if not event["type"] in ("chat", "normal", "groupchat"): if not event["type"] in ("chat", "normal", "groupchat"):
@ -237,15 +279,17 @@ class Hptoad:
if event["type"] == "groupchat": if event["type"] == "groupchat":
nick = event["mucnick"] nick = event["mucnick"]
self.handle_muc_message(body, nick, from_id) if nick != self.bot_nick:
yield from self.handle_muc_message(body, nick, from_id)
elif event["from"].bare == self.jid: elif event["from"].bare == self.jid:
# Use resource as a nickname with self messages. # Use resource as a nickname with self messages.
nick = from_id.resource nick = from_id.resource
self.handle_self_message(body, nick, from_id) yield from self.handle_self_message(body, nick, from_id)
except Exception as e: except Exception as e:
self.log_exception(e) self.log_exception(e)
@asyncio.coroutine
def on_muc_presence(self, event): def on_muc_presence(self, event):
try: try:
typ = event["muc"]["type"] typ = event["muc"]["type"]
@ -255,51 +299,67 @@ class Hptoad:
if not typ: if not typ:
typ = event["type"] typ = event["type"]
if not nick: if not nick:
nick = self.muc_obj.getNick(self.muc, from_id) nick = self.muc_obj.get_nick(self.muc, from_id)
if typ == "error": if typ == "available":
self.muc_is_joined = True
elif typ == "error":
self.muc_is_joined = False
if event["error"]["code"] == "409": if event["error"]["code"] == "409":
self.bot_nick = self.bot_nick + "_" self.bot_nick = self.bot_nick + "_"
self.join_muc() self.join_muc()
elif typ == "unavailable": elif typ == "unavailable":
if nick == self.bot_nick: if nick == self.bot_nick:
self.muc_is_joined = False
self.bot_nick = self.pure_bot_nick self.bot_nick = self.pure_bot_nick
time.sleep(0.5) yield from asyncio.sleep(0.5)
self.join_muc() self.join_muc()
except Exception as e: except Exception as e:
self.log_exception(e) self.log_exception(e)
def on_plugin_got_result(self, future, nick="", from_id="", is_admin=False):
result = future.result()
if not result:
return
if result["handled"] and result["reply"]:
self.client.send_message(mto=self.muc, mbody=result["reply"],
mtype="groupchat")
if result["error"]:
self.logger.error(result["error"])
if is_admin and from_id:
self.client.send_message(mto=from_id, mbody=result["error"],
mtype="chat")
def import_plugins(self):
plugins = {}
_, _, filenames = next(os.walk("./plugins"), (None, None, []))
for filename in (i for i in filenames if i.endswith(".py")):
try:
plugin = HptoadPlugin(filename[:-3])
plugins[filename[:-3]] = plugin
future = asyncio.async(plugin.call_initiate())
future.add_done_callback(self.on_plugin_got_result)
except Exception as e:
self.log_exception(e)
return plugins
def run(self): def run(self):
# Reset the nick. self.plugins = self.import_plugins()
self.bot_nick = self.pure_bot_nick
if self.connect:
connect = self.connect.split(":", 1)
if len(connect) != 2 or not connect[1].isdigit():
logging.critical("Conn: Connection server format is " +
"invalid, should be example.org:5222")
sys.exit(1)
else:
connect = ()
if self.client.connect(connect):
self.register_handlers() self.register_handlers()
self.client.process(block=True) self.connect()
else: self.client.process(forever=True)
logging.critical("Auth: Could not connect to server, or " +
"password mismatch!")
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
logging.basicConfig(level=logging.DEBUG, logging.basicConfig(format="%(asctime)s %(message)s",
format="%(asctime)s %(message)s",
datefmt="%Y/%m/%d %H:%M:%S") datefmt="%Y/%m/%d %H:%M:%S")
# Silence sleekxmpp debug information.
logging.getLogger("sleekxmpp").setLevel(logging.CRITICAL)
if os.path.isfile(sys.argv[0]) and os.path.dirname(sys.argv[0]): if os.path.isfile(sys.argv[0]) and os.path.dirname(sys.argv[0]):
os.chdir(os.path.dirname(sys.argv[0])) os.chdir(os.path.dirname(sys.argv[0]))
@ -308,4 +368,3 @@ if __name__ == "__main__":
while True: while True:
hptoad.run() hptoad.run()
logging.error("Unknown: WTF am I doing here?") logging.error("Unknown: WTF am I doing here?")
time.sleep(0.5)

62
plugins/shell_cmds.py Normal file
View File

@ -0,0 +1,62 @@
# -*- python -*-
import asyncio
import os
import re
class Plugin:
_trim_regexp = re.compile("(`|\\$|\\.\\.)")
_quote_regexp = re.compile("(\"|')")
@classmethod
def _trim(cls, s):
result = cls._trim_regexp.sub("", s)
result = cls._quote_regexp.sub("", result).strip()
return result
# letter(ASCII or cyrillic), number, underscore only.
_cmd_validator_regexp = re.compile("^(\\w|\\p{Cyrillic})*$")
@asyncio.coroutine
def _exec_cmd(self, cmd, body, nick, dir_path, is_admin):
is_admin = "true" if is_admin else "false"
path = os.path.join(dir_path, self._trim(cmd))
if not self._cmd_validator_regexp.match(cmd) or \
not os.access(path, os.F_OK | os.X_OK) or not os.path.isfile(path):
return {"handled": False}
if not os.access(path, os.R_OK):
return {"handled": True,
"error": "\"%s\" is not readable" % path}
cmd = [path, self._trim(nick), is_admin, self._trim(body)]
try:
pipe = asyncio.subprocess.PIPE
proc = yield from asyncio.create_subprocess_exec(*cmd,
stdout=pipe,
stderr=pipe)
cmd_reply, cmd_error = yield from proc.communicate()
except OSError as e:
return {"handled": True,
"error": "Execute: %s" % str(e)}
result = {}
if cmd_error and len(cmd_error.strip()) > 0:
result["error"] = "Process: %s" % cmd_error.strip()
if cmd_reply and len(cmd_reply.strip()) > 0:
result["reply"] = cmd_reply.decode().strip()
if result:
result["handled"] = True
return result
@asyncio.coroutine
def command(self, command, body, nick, from_id, is_admin):
result = yield from self._exec_cmd(command, body, nick,
"plugins", is_admin)
return result
@asyncio.coroutine
def question(self, body, nick, from_id, is_admin):
result = yield from self._exec_cmd("answer", body, nick,
"chat", is_admin)
return result

60
plugins/title_fetch.py Normal file
View File

@ -0,0 +1,60 @@
# -*- python -*-
import asyncio
import bs4
import functools
import lxml
import re
import requests
class Plugin:
_html_regexp = re.compile(r"(https?://[^\"\s>]+)")
@asyncio.coroutine
def chat_message(self, body, nick, from_id, is_admin):
loop = asyncio.get_event_loop()
result = {}
urls = self._html_regexp.findall(body)
if urls:
result["handled"] = True
mime_types = ("application/xhtml+xml", "application/xml",
"text/html", "text/xml")
reply = ""
for url in urls:
try:
req = yield from \
loop.run_in_executor(None,
functools.partial(requests.get,
url,
stream=True))
if req.headers["content-type"].startswith(mime_types):
# Handle a case when no charset is defined for text/html.
if req.headers["content-type"].startswith("text/") and \
not "charset=" in req.headers["content-type"]:
req.encoding = None
if not req.encoding:
req.encoding = req.apparent_encoding
contents = title = ""
for i in req.iter_content(chunk_size=128,
decode_unicode=True):
contents += i
soup = bs4.BeautifulSoup(contents, "lxml")
if soup and soup.title:
if soup.title.string == title:
req.close()
break
title = soup.title.string
if title:
if reply:
reply += "\n"
reply += "Link: %s" % title
except Exception as e:
result["error"] = "Title fetch: %s" % str(e)
result["reply"] = reply
else:
result["handled"] = False
return result

View File

@ -1,2 +1 @@
sleekxmpp>=1.2.0 slixmpp
grab