From 9f2ea9cc1de4bc73873f3f35b1469779c77aac2a Mon Sep 17 00:00:00 2001 From: feder Date: Sat, 18 Feb 2017 21:35:12 +0300 Subject: [PATCH] Fork project --- COPYING | 21 +++ README.md | 5 + examples/chat/answer | 51 +++++++ examples/chat/frs.txt | 0 examples/chat/pictures.txt | 0 examples/plugins/example | 11 ++ gsend.py | 55 +++++++ hptoad.py | 289 +++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 9 files changed, 433 insertions(+) create mode 100644 COPYING create mode 100644 README.md create mode 100755 examples/chat/answer create mode 100644 examples/chat/frs.txt create mode 100644 examples/chat/pictures.txt create mode 100755 examples/plugins/example create mode 100755 gsend.py create mode 100755 hptoad.py create mode 100644 requirements.txt diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..4f65e07 --- /dev/null +++ b/COPYING @@ -0,0 +1,21 @@ +Copyright (c) 2014 cxindex +Copyright (c) 2015 Alexey Kharlamov +Copyright (c) 2016 Alexei Sorokin + +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 +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3a5af09 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# hptoad + +An MIT licensed XMPP bot written using Python and sleekxmpp. + +Original project: https://gitlab.com/XRevan86/hptoad diff --git a/examples/chat/answer b/examples/chat/answer new file mode 100755 index 0000000..c28a0f0 --- /dev/null +++ b/examples/chat/answer @@ -0,0 +1,51 @@ +#!/bin/bash + +# Simple chat bot +# with sending images to an external programme. + +random() { + if (( "$1" < 1 )); then + echo '1' + else + shuf -n1 -i "1-$1" + fi +} + +cd "$(dirname "$0")" + +printf "%s" "$1: " +from=$(printf "%s\n" "$1" | tr -d '$`|<>') + +# Second argument shows whether user is an admin (useless). +shift 2 +string=$(echo "$@" | tr -d '$`') +if [[ ${#string} -gt 750 ]]; then + echo "tl;dr" + exit 0 +fi + +if [[ "$string" == *http*://* ]]; then + amount=$(sed -e '/https*:\/\//!d' ./frs.txt | wc -l) + # Makes it a bit more human (time to find a link?). + sleep 1 + sed -e '/https*:\/\//!d' ./frs.txt | sed -n "$(random "$amount")p" + img="$(printf "%s\n" "$string" | sed -ne 's|.*\(https*://[^ \"()<>]*\).*|\1|g;1p')" + ns="$(printf "%s\n" "$string" | sed -e "s|$img||g")" + cl="$(wget --spider -S "$img" 2>&1)" + if [[ $(printf "%s\n" "$cl" | sed -e '/Content-Type/!d;/image\//!d') ]]; then + #echo 'posting' + printf "%s\n" "$img $from" >> ./pictures.txt + fi +else + # Exclude last two entries from the amount. + amount="$(($(sed -e '/https*:\/\//d' ./frs.txt | wc -l) - 2))" + answer="$(sed -e '/https*:\/\//d' ./frs.txt | sed -ne "s/;;\\\n/\n/g;$(random "$amount")p")" + # Makes it a bit more human. + sleep "$(echo "${#answer} * 0.15" | bc -l)" + printf "%s\n" "$answer" +fi +if [[ ${#string} -lt 7 ]]; then + exit 0 +fi +printf "%s\n" "$string" | sed -e '{:q;N;s/\n/;;\\n/g;t q}' >> ./frs.txt +exit 0 diff --git a/examples/chat/frs.txt b/examples/chat/frs.txt new file mode 100644 index 0000000..e69de29 diff --git a/examples/chat/pictures.txt b/examples/chat/pictures.txt new file mode 100644 index 0000000..e69de29 diff --git a/examples/plugins/example b/examples/plugins/example new file mode 100755 index 0000000..2edcbcb --- /dev/null +++ b/examples/plugins/example @@ -0,0 +1,11 @@ +#!/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 + diff --git a/gsend.py b/gsend.py new file mode 100755 index 0000000..4428570 --- /dev/null +++ b/gsend.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import sleekxmpp + +opts = { + "jid": "botname@example.com", + "resource": "resource", + "password": "password", + "connect": "xmpp.example.org:5222", +} + + +def on_session_start(event): + client.get_roster() + # client.send_presence() + + body = "\n".join(sys.argv[1:]).strip() + try: + if body: + client.send_message(mto=opts["jid"], mbody=body, mtype="chat") + except Exception as e: + print("%s: %s" % (type(e).__name__, str(e))) + finally: + client.disconnect(wait=True) + sys.exit(0) + + +if __name__ == "__main__": + if sys.version_info.major < 3: + sleekxmpp.util.misc_ops.setdefaultencoding("utf-8") + + if len(sys.argv) <= 1: + print("At least one argument is required.") + sys.exit(1) + + client = sleekxmpp.ClientXMPP("%s/%s" % (opts["jid"], opts["resource"]), + opts["password"]) + + if opts["connect"]: + connect = opts["connect"].split(":", 1) + if len(connect) != 2 or not connect[1].isdigit(): + print("Connection server format is invalid, should be " + + "example.org:5222") + sys.exit(1) + else: + connect = () + + if client.connect(connect): + client.add_event_handler("session_start", on_session_start) + client.process(block=True) + else: + print("Could not connect to server, or password mismatch!") + sys.exit(1) diff --git a/hptoad.py b/hptoad.py new file mode 100755 index 0000000..1c8b337 --- /dev/null +++ b/hptoad.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import logging +import os +import re +import signal +import subprocess +import sys +import time +import sleekxmpp + +opts = { + "muc": "room@conference.example.com", + "nick": "botname", + "jid": "botname@example.com", + "resource": "resource", + "password": "password", + "connect": "xmpp.example.org:5222", +} + + +class Hptoad: + def __init__(self, opts): + if sys.version_info.major < 3: + sleekxmpp.util.misc_ops.setdefaultencoding("utf-8") + + self.client = sleekxmpp.ClientXMPP("%s/%s" % (opts["jid"], + opts["resource"]), + opts["password"]) + self.client.register_plugin("xep_0199") # XMPP Ping. + self.client.register_plugin("xep_0045") # XMPP MUC. + self.muc_obj = self.client.plugin["xep_0045"] + + self.jid = opts["jid"] + self.connect = opts["connect"] + self.muc = opts["muc"] + self.pure_bot_nick = opts["nick"] + self.bot_nick = self.pure_bot_nick + + def register_handlers(self): + self.client.add_event_handler("session_start", self.on_session_start) + self.client.add_event_handler("message", self.on_message, + threaded=True) + self.client.add_event_handler("muc::%s::presence" % self.muc, + self.on_muc_presence) + + def join_muc(self): + if self.muc in self.muc_obj.getJoinedRooms(): + 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(cls, ex): + logging.error("%s: %s" % (type(ex).__name__, str(ex))) + + @classmethod + def log_message_event(cls, event): + logging.debug("&{{jabber:client message} %s %s %s %s %s { }}" % + (event["from"], event["id"], event["to"], + event["type"], event["body"])) + + def is_muc_admin(self, muc, nick): + if nick not in self.muc_obj.rooms[self.muc]: + return False + + affiliation = self.muc_obj.getJidProperty(muc, nick, "affiliation") + return True if affiliation in ("admin", "owner") else False + + _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})*$") + + 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_victim_admin = self.is_muc_admin(self.muc, victim) + + if is_admin and victim != self.bot_nick: + if is_bot_admin and not is_victim_admin and \ + victim in self.muc_obj.rooms[self.muc]: + self.muc_obj.setRole(self.muc, victim, "none") + else: + reply = "%s: Can't megakick %s." % (nick, victim) + else: + 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: + msg = body.strip() + + 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 + + # 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: + self.client.send_message(mto=self.muc, mbody=reply, + mtype="groupchat") + + def on_session_start(self, event): + self.client.get_roster() + self.client.send_presence(pstatus="is there some food in this world?", + ppriority=12) + self.join_muc() + + def on_message(self, event): + try: + if not event["type"] in ("chat", "normal", "groupchat"): + return + + self.log_message_event(event) + + body = event["body"] + from_id = event["from"] + + if event["type"] == "groupchat": + nick = event["mucnick"] + self.handle_muc_message(body, nick, from_id) + + elif event["from"].bare == self.jid: + # Use resource as a nickname with self messages. + nick = from_id.resource + self.handle_self_message(body, nick, from_id) + except Exception as e: + self.log_exception(e) + + def on_muc_presence(self, event): + try: + typ = event["muc"]["type"] + from_id = event["from"] + nick = event["muc"]["nick"] + + if not typ: + typ = event["type"] + if not nick: + nick = self.muc_obj.getNick(self.muc, from_id) + + if typ == "error": + if event["error"]["code"] == "409": + self.bot_nick = self.bot_nick + "_" + self.join_muc() + + elif typ == "unavailable": + if nick == self.bot_nick: + self.bot_nick = self.pure_bot_nick + time.sleep(0.5) + self.join_muc() + except Exception as e: + self.log_exception(e) + + def run(self): + # Reset the nick. + 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.client.process(block=True) + else: + logging.critical("Auth: Could not connect to server, or " + + "password mismatch!") + sys.exit(1) + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal.SIG_DFL) + + logging.basicConfig(level=logging.DEBUG, + format="%(asctime)s %(message)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]): + os.chdir(os.path.dirname(sys.argv[0])) + + hptoad = Hptoad(opts) + while True: + hptoad.run() + logging.error("Unknown: WTF am I doing here?") + time.sleep(0.5) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86b3814 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +sleekxmpp>=1.2.0