Fork project

This commit is contained in:
feder 2017-02-18 21:35:12 +03:00
commit 9f2ea9cc1d
9 changed files with 433 additions and 0 deletions

21
COPYING Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2014 cxindex
Copyright (c) 2015 Alexey Kharlamov <derlafff@ya.ru>
Copyright (c) 2016 Alexei Sorokin <sor.alexei@meowr.ru>
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.

5
README.md Normal file
View File

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

51
examples/chat/answer Executable file
View File

@ -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

0
examples/chat/frs.txt Normal file
View File

View File

11
examples/plugins/example Executable file
View File

@ -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

55
gsend.py Executable file
View File

@ -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)

289
hptoad.py Executable file
View File

@ -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)

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
sleekxmpp>=1.2.0