Merge branch 'develop'

This commit is contained in:
aewens 2018-09-16 00:28:01 -04:00
commit 2453aa1601
17 changed files with 945 additions and 120 deletions

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
# App-specific
settings.json
settings.demo.json
data/*.json
irc/
# ---> Python
# Byte-compiled / optimized / DLL files

View File

@ -1,4 +1,4 @@
Copyright (c) <year> <owner> . All rights reserved.
Copyright (c) 2018 Austin Ewens. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

63
actions/__init__.py Normal file
View File

@ -0,0 +1,63 @@
from actions.botlist import botlist
from actions.web import summon, whois
from actions.access import banish, pardon
from actions.control import puppet, nomad
from actions.stupid import hmm, hmmscore, hmmscoreboard
actions = [
{
"type": "response",
"pattern": "!botlist",
"callback": botlist
},
{
"type": "response",
"pattern": "!rollcall",
"callback": botlist
},
{
"type": "response",
"pattern": ";;!summon \S+ .+",
"callback": summon
},
{
"type": "response",
"pattern": ";;!banish \S+ .+",
"callback": banish
},
{
"type": "response",
"pattern": ";;!pardon \S+",
"callback": pardon
},
{
"type": "response",
"pattern": ";;!puppet [^|]+\|.+",
"callback": puppet
},
{
"type": "response",
"pattern": ";;!nomad \S+ \S+",
"callback": nomad
},
{
"type": "response",
"pattern": ";;hm+",
"callback": hmm
},
{
"type": "response",
"pattern": ";;!hmmscore",
"callback": hmmscore
},
{
"type": "response",
"pattern": "!hmmscoreboard",
"callback": hmmscoreboard
},
{
"type": "response",
"pattern": ";;!whois \S+",
"callback": whois
}
]

37
actions/access.py Normal file
View File

@ -0,0 +1,37 @@
from datetime import datetime
def banish(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
user, reason = response.split("!banish ")[1].split(" ", 1)
if name != author:
return
if user not in self.bot.memories["users"]:
self.bot.memories["users"][user] = dict()
self.bot.memories["users"][user]["blacklist"] = {
"reason": reason,
"when": datetime.now().timestamp()
}
self.bot.save_memories()
confirmation = "{} has been banished for reason: {}".format(user, reason)
self.bot.send_message(source, confirmation)
def pardon(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
user = response.split("!pardon ")[1]
if name != author:
return
del self.bot.memories["users"][user]["blacklist"]
self.bot.save_memories()
confirmation = "{} has been pardoned".format(user)
self.bot.send_message(source, confirmation)

18
actions/botlist.py Normal file
View File

@ -0,0 +1,18 @@
def botlist(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
email = self.bot.settings["email"]
about = "the meta chat bot"
commands = ", ".join([
"!botlist",
"!rollcall",
"!summon",
"!banish",
"!pardon",
"!hmmscore",
"!hmmscoreboard",
"!whois"
])
args = (botnick, author, email, about, commands)
message = "{} | {} <{}> | {} | {}".format(*args)
self.bot.send_message(source, message)

27
actions/control.py Normal file
View File

@ -0,0 +1,27 @@
def puppet(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
command = response.split("!puppet ")[1]
place, message = command.split("|", 1)
if name != author:
return
self.bot.send_message(place, message)
def nomad(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
command = response.split("!nomad ")[1]
action, place = command.split(" ", 1)
if name != author:
return
actions = {
"join": self.bot.join,
"leave": self.bot.leave
}
default = lambda p: self.bot.send_message(source, "Invalid action!")
actions.get(action, default)(place)

58
actions/stupid.py Normal file
View File

@ -0,0 +1,58 @@
import re
import operator
def hmm(self, name, source, response):
botnick = self.bot.botnick
pattern = re.compile("hm+")
matches = re.findall(pattern, response)
score = len(matches)
if name not in self.bot.memories["users"]:
self.bot.memories["users"][name] = dict()
if "hmmscore" not in self.bot.memories["users"][name]:
self.bot.memories["users"][name]["hmmscore"] = 0
current_score = self.bot.memories["users"][name]["hmmscore"]
self.bot.memories["users"][name]["hmmscore"] = current_score + score
self.bot.save_memories()
def hmmscore(self, name, source, response):
botnick = self.bot.botnick
score = 0
score_format = "Hmm score for '{}': {}"
if " " in response:
name = response.split(" ", 1)[1]
if name not in self.bot.memories["users"]:
self.bot.send_message(source, score_format.format(name, score))
return
if "hmmscore" in self.bot.memories["users"][name]:
score = self.bot.memories["users"][name]["hmmscore"]
self.bot.send_message(source, score_format.format(name, score))
return
def hmmscoreboard(self, name, source, response):
botnick = self.bot.botnick
hmmscores = list()
for user, values in self.bot.memories["users"].items():
hmmscores.append({
"name": user,
"score": values.get("hmmscore", 0)
})
size = 3
start = -size
sort_scores = sorted(hmmscores, key=lambda k: k["score"])
top_scores = sort_scores[start:][::-1]
leaders = " | ".join([
"{} {}".format(ts["name"], ts["score"]) for ts in top_scores
])
self.bot.send_message(source, "Hmm Score Leaderboard: {}".format(leaders))

101
actions/web.py Normal file
View File

@ -0,0 +1,101 @@
from subprocess import Popen, PIPE
from email.mime.text import MIMEText
from urllib.request import Request, urlopen
from urllib.parse import urlencode
from urllib.error import HTTPError
from json import loads
def get_iden(devices, device_name):
for device in devices:
if device.get("nickname", "") == device_name:
return device.get("iden", "")
def push_note(bot, title, body):
api_url = "https://api.pushbullet.com/v2"
extra_settings = bot.settings.get("extras", dict())
pb_settings = extra_settings.get("pushbullet", dict())
api_key = pb_settings.get("api", "")
device_name = pb_settings.get("device", "")
list_devices = Request("{}/devices".format(api_url))
list_devices.add_header("Access-Token", api_key)
try:
data = loads(urlopen(list_devices).read())
except HTTPError:
return
devices = data.get("devices", list())
iden = get_iden(devices, device_name)
params = {
"device_iden": iden,
"type": "note",
"title": title,
"body": body
}
post_params = urlencode(params).encode()
pushes = Request("{}/pushes".format(api_url), post_params)
pushes.add_header("Access-Token", api_key)
try:
response = loads(urlopen(pushes).read())
except HTTPError as e:
return
def summon(self, name, source, response):
botnick = self.bot.botnick
author = self.bot.author
user, reason = response.split("!summon ")[1].split(" ", 1)
email = "{}@tilde.team"
subject = "You have been summoned!"
text = " ".join([
"My bot, {}, received a summoning request for you".format(botnick),
"from {} in channel {} for reason: {}".format(name, source, reason)
])
message = MIMEText(text)
message["From"] = email.format(botnick)
message["To"] = email.format(user)
message["Subject"] = subject
command = "/usr/sbin/sendmail -t -oi".split(" ")
p = Popen(command, stdin=PIPE, universal_newlines=True)
p.communicate(message.as_string())
if user == author:
push_note(self.bot, subject, text)
confirmation = "{}: You have summoned {}".format(name, user)
self.bot.send_message(source, confirmation)
def whois(self, name, source, response):
botnick = self.bot.botnick
domain = response.split("!whois ")[1]
api_url = "https://api.jsonwhoisapi.com/v1/whois"
api_key = self.bot.settings.get("extras", dict()).get("jsonwhoisapi", "")
req = Request("{}?identifier={}".format(api_url, domain))
req.add_header("Authorization", api_key)
try:
data = loads(urlopen(req).read())
except HTTPError:
self.bot.send_message(source, "{} cannot exist".format(domain))
return
registered = data.get("registered", None)
if registered is not None:
nameservers = len(data.get("nameservers", ""))
registrar = data.get("registrar", dict())
is_registered = "id" in registrar or nameservers > 0
status = "registered" if is_registered else "available"
self.bot.send_message(source, "{} is '{}'".format(domain, status))
else:
self.bot.send_message(source, "{} might be available".format(domain))

85
app.py
View File

@ -1,12 +1,85 @@
#!/usr/bin/env python3
from bot import Bot
from os.path import dirname, realpath
bot = Bot("127.0.0.1", 6667, "BabiliBot|py", ["#bots"])
from bot import Bot, Tasks, Responses
from actions import actions
from coroutines import coroutines
def processor(name, source, response):
#bot.send_message(source, "Got response")
print(name, source, response)
debug = False
kingme = [] if debug else ["#chaos"]
channels = ["#bots", "#insane"]
if not debug:
channels.extend([
"#meta",
"#team",
"#chaos",
"#tildeverse"
])
bot = Bot("127.0.0.1", 6667, "BabiliBot", channels)
responses = Responses(bot)
tasks = Tasks(bot)
for action in actions:
if "type" in action and "pattern" in action and "callback" in action:
responses.add_trigger(
action["type"],
action["pattern"],
action["callback"]
)
# for coro in coroutines:
# worker = coro["worker"]
# interval = coro["interval"]
# state = coro.get("state", None)
# coro_state = state if state is not None else (bot,)
# tasks.add_coroutine(worker, interval, coro_state)
tasks.coroutines = coroutines
def try_to_king_me(channel):
bot.send_message("ChanServ", "REGISTER {}", channel)
bot.send_message("ChanServ", "SET Successor {} {}", channel, bot.botnick)
bot.send_message("ChanServ", "SET Founder {} {}", channel, bot.author)
def handle_pm(name, response):
print("PM: {} - {}".format(name, response))
def handle_mode(channel, mode):
if mode == "-r":
try_to_king_me(channel)
def handle_invite(channel, name):
if channel in kingme:
try_to_king_me(channel)
users = bot.memories["users"]
if name not in users:
bot.memories["users"][name] = dict()
if "invites" not in users[name]:
bot.memories["users"][name]["invites"] = list()
bot.memories["users"][name]["invites"].append(channel)
def handle_kick(name):
users = bot.memories["users"]
if name not in users:
bot.memories["users"][name] = dict()
bot.memories["users"][name]["kicker"] = True
def handle_message(name, source, response):
responses.parse(name, source, response)
if response == "!debug":
print("::", bot.memories)
if __name__ == "__main__":
bot.start(processor, "settings.json")
bot.tasks = tasks
bot.start(dirname(realpath(__file__)), {
"pm": handle_pm,
"mode": handle_mode,
"invite": handle_invite,
"kick": handle_kick,
"message": handle_message
})

View File

@ -1,113 +1,3 @@
import json
import socket
class Bot:
def __init__(self, server, port, botnick, channels):
self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server = server
self.port = port
self.botnick = botnick
self.channels = channels
self.running = True
self.users = dict()
self.recv_size = 2048
def send(self, message, *args):
response = message.format(*args) + "\n"
print("DEBUG: ", response)
self.ircsock.send(response.encode())
def send_message(self, target, message, *args):
msg = message.format(*args)
response = "PRIVMSG {0} :{1}".format(target, msg) + "\n"
print("DEBUG: ", response)
self.ircsock.send(response.encode())
def join(self, chan, confirmed=False):
self.send("JOIN {}", chan)
message = ""
magic_string = "End of /NAMES list."
while magic_string not in message:
message = self.ircsock.recv(self.recv_size).decode()
message = message.strip("\n\r")
print(message)
user_list = "= {} :".format(chan)
raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ")
prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u
users = list(filter(prefix_filter, raw_users))
for user in users:
if user not in self.users:
self.users[user] = dict()
def ping(self, message):
response = message.split("PING :")[1]
self.send("PONG :{0}", response)
def parse(self, message):
before, after = message.split("PRIVMSG ", 1)
name = before.split("!", 1)[0][1:]
source, response = after.split(" :", 1)
return name, source, response
def track_rename(self, message):
# :aewens|otg!aewensotg@tilde.team NICK aewens|otg2
before, new_name = message.split("NICK ", 1)
name = before.split("!", 1)[0][1:]
user = self.users[name]
del self.users[name]
self.users[new_name] = user
def load_settings(self, location):
with open(location, "r") as f:
self.settings = json.loads(f.read())
def start(self, callback, settings):
message = ""
registered = False
confirmed = True
self.ircsock.connect((self.server, self.port))
self.send("USER {0} {0} {0} {0}", self.botnick)
self.send("NICK {0}", self.botnick)
self.load_settings(settings)
password = self.settings["password"] or ""
confirm = self.settings["confirm"] or ""
email = self.settings["email"] or ""
magic_string = "MODE {} +r".format(self.botnick)
while magic_string not in message:
message = self.ircsock.recv(self.recv_size).decode()
message = message.strip("\n\r")
print(message)
if not registered and "Password accepted" in message:
registered = True
if not registered and "choose a different nick" in message:
self.send_message("NickServ", "IDENTIFY {}", password)
if not confirmed and "Your account will expire" in message:
self.send_message("NickServ", "CONFIRM {}", self.confirm)
confirmed = True
print("DEBUG: Joining")
for channel in self.channels:
self.join(channel)
print("DEBUG: Joined")
while self.running:
message = self.ircsock.recv(self.recv_size).decode("UTF-8")
message = message.strip("\n\r")
print(message)
if "PING :" in message:
self.ping(message)
elif "NICK " in message:
self.track_rename(message)
elif "PRIVMSG" in message:
name, source, response = self.parse(message)
callback(name, source, response)
from bot.core import Bot
from bot.tasks import Tasks
from bot.responses import Responses

246
bot/core.py Normal file
View File

@ -0,0 +1,246 @@
import re
import json
import socket
import os.path
class Bot:
def __init__(self, server, port, botnick, channels):
self.ircsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server = server
self.port = port
self.botnick = botnick
self.channels = channels
self.running = True
self.settings = dict()
self.places = list()
self.tasks = None
self.author = ""
self.recv_size = 2048
self.splitter = "\n\r"
def send(self, message, *args):
response = message.format(*args) + "\n"
print("DEBUG: ", response)
self.ircsock.send(response.encode())
def send_message(self, target, message, *args):
msg = message.format(*args)
response = "PRIVMSG {0} :{1}".format(target, msg) + "\n"
print("DEBUG: ", response)
self.ircsock.send(response.encode())
def send_action(self, target, message, *args):
self.send_message(target, "\001ACTION {}\001".format(message), *args)
def join(self, chan):
self.send("JOIN {}", chan)
self.places.append(chan)
message = ""
magic_string = "End of /NAMES list."
while magic_string not in message:
message = self.ircsock.recv(self.recv_size).decode()
message = message.strip(self.splitter)
print(message)
user_list = "= {} :".format(chan)
raw_users = message.split(user_list)[1].split(" \r\n")[0].split(" ")
prefix_filter = lambda u: u[1:] if "~" in u or "@" in u else u
users = list(filter(prefix_filter, raw_users))
remember = self.memories["users"]
for user in users:
if user[0] == "~" or user[0] == "@":
user = user[1:]
if user not in remember:
self.memories["users"][user] = dict()
def leave(self, chan):
message = "PART {} :Bye-bye!"
self.send(message, chan)
self.places.remove(chan)
def ping(self, message):
response = message.split("PING :")[1]
self.send("PONG :{0}", response)
def get_name(self, text):
return text.split("!", 1)[0][1:]
def parse(self, message):
before, after = message.split("PRIVMSG ", 1)
name = self.get_name(before)
source, response = after.split(" :", 1)
return name, source, response
def handle_mode(self, message):
before, after = message.split("MODE ", 1)
name = self.get_name(before)
channel, mode = after.split(" ")[:2]
return channel, mode
def handle_rename(self, message):
before, new_name = message.split("NICK ", 1)
name = self.get_name(before)
user = self.memories["users"][name]
del self.memories["users"][name]
self.memories["users"][new_name] = user
return user, new_name
def handle_invite(self, message):
before, after = message.split("INVITE ", 1)
name = self.get_name(before)
channel = after.split(":", 1)[1]
self.join(channel)
return channel, name
def handle_kick(self, message):
regex = "KICK #\S+ {} :".format(self.botnick)
before, kicker = re.split(regex, message)
return kicker
def handle_join(self, message):
before, after = message.split("JOIN ", 1)
user = self.get_name(before)
if user not in self.memories["users"]:
self.memories["users"][user] = dict()
return user
def handle_part(self, message):
before, after = message.split("PART ", 1)
user = self.get_name(before)
return user
def load_memories(self, location):
path = "{}/{}".format(self.location, location)
self.memories_path = path
if not os.path.isfile(path):
self.memories = {
"users": dict()
}
else:
with open(path, "r") as f:
self.memories = json.loads(f.read())
def save_memories(self):
with open(self.memories_path, "w") as f:
try:
f.write(json.dumps(self.memories))
except ValueError as e:
f.write("")
def load_settings(self, location):
set_vars = [
"author"
]
path = "{}/{}".format(self.location, location)
with open(path, "r") as f:
self.settings = json.loads(f.read())
for name, attr in self.settings.items():
if name in set_vars:
setattr(self, name, attr)
def stop(self):
self.running = False
self.send("QUIT")
def start(self, location, callback):
message = ""
registered = False
confirmed = True
self.ircsock.connect((self.server, self.port))
self.send("USER {0} {0} {0} {0}", self.botnick)
self.send("NICK {0}", self.botnick)
self.location = location
self.load_settings("settings.json")
self.load_memories("data/memories.json")
password = self.settings["password"] or ""
confirm = self.settings["confirm"] or ""
email = self.settings["email"] or ""
magic_phrase = {
"has_registered": "Password",
"needs_to_register": "choose a different nick",
"needs_to_confirm": "Your account will expire"
}
magic_string = "MODE {} +r".format(self.botnick)
while magic_string not in message:
message = self.ircsock.recv(self.recv_size).decode()
message = message.strip(self.splitter)
print(message)
if not registered and magic_phrase["has_registered"] in message:
registered = True
if not registered and magic_phrase["needs_to_register"] in message:
self.send_message("NickServ", "IDENTIFY {}", password)
if not confirmed and magic_phrase["needs_to_confirm"] in message:
self.send_message("NickServ", "CONFIRM {}", self.confirm)
confirmed = True
self.send("MODE {} +B".format(self.botnick))
print("DEBUG: Joining")
for channel in self.channels:
self.join(channel)
print("DEBUG: Joined")
if self.tasks is not None:
if getattr(self.tasks, "run", None) is not None:
self.tasks.run()
while self.running:
message = self.ircsock.recv(self.recv_size).decode()
message = message.strip(self.splitter)
print(message)
if "raw" in callback:
callback["raw"](message)
if "PING :" in message:
self.ping(message)
if "ping" in callback:
callback["ping"]()
elif "MODE " in message:
channel, mode = self.handle_mode(message)
if "mode" in callback:
callback["mode"](channel, mode)
elif "NICK " in message:
old_name, new_name = self.handle_rename(message)
if "nick" in callback:
callback["nick"](old_name, new_name)
elif "KICK " in message:
kicker = self.handle_kick(message)
if "kick" in callback:
callback["kick"](kicker)
elif "JOIN " in message:
user = self.handle_join(message)
if "join" in callback:
callback["join"](user)
elif "PART " in message:
user = self.handle_part(message)
if "part" in callback:
callback["part"](user)
elif "INVITE " in message:
channel, name = self.handle_invite(message)
if "invite" in callback:
callback["invite"](channel, name)
elif "PRIVMSG " in message:
name, source, response = self.parse(message)
if source == self.botnick and "pm" in callback:
callback["pm"](name, response)
elif "message" in callback:
callback["message"](name, source, response)
elif "unhandled" in callback:
callback["unhandled"](message)

73
bot/responses.py Normal file
View File

@ -0,0 +1,73 @@
import re
from datetime import datetime
class Responses:
def __init__(self, bot):
self.bot = bot
self.triggers = {
"name": dict(),
"source": dict(),
"response": dict()
}
def add_trigger(self, trigger_type, pattern, callback):
if trigger_type in self.triggers:
self.triggers[trigger_type][pattern] = callback
def allowed(self, name, source):
memories = self.bot.memories
users = memories["users"]
if name not in users:
return False
if name in users and "blacklist" in users[name]:
reason = users[name]["blacklist"]["reason"]
message = "is ignoring {} for reason '{}'".format(name, reason)
self.bot.send_action(source, message)
return False
last_response = 0
if "last_response" in self.bot.memories["users"][name]:
last_response = self.bot.memories["users"][name]["last_response"]
now = datetime.now().timestamp()
author = self.bot.author
wait = 1
if name != author and last_response > 0 and now - last_response < wait:
self.bot.memories["users"][name]["blacklist"] = {
"reason": "Auto-banished",
"when": now
}
return False
return True
def parse(self, name, source, response):
users = self.bot.memories["users"]
if name not in users:
return False
response = response.lower()
check = {
"name": name,
"source": source,
"response": response
}
marker = ";;"
mlen = len(marker)
for trigger in list(self.triggers.keys()):
for pattern, callback in self.triggers[trigger].items():
if pattern[:mlen] != marker:
if pattern == response and self.allowed(name, source):
callback(self, name, source, response)
else:
regex = re.compile(pattern[mlen:])
if regex.match(check[trigger]) is not None:
if self.allowed(name, source):
callback(self, name, source, response)
now = datetime.now().timestamp()
self.bot.memories["users"][name]["last_response"] = now

37
bot/tasks.py Normal file
View File

@ -0,0 +1,37 @@
import time
import sched
from threading import Thread
class Tasks:
def __init__(self, bot):
self.bot = bot
self.scheduler = sched.scheduler(time.time, time.sleep)
self.thread = Thread(target=self.worker, args=(self,))
self.coroutines = list()
self.states = dict()
def periodic(self, scheduler, interval, action, index, state=dict()):
self.states[index] = action(state)
scheduler.enter(interval, 1, self.periodic, (
scheduler, interval, action, index, self.states[index]
))
def worker(self, tasks):
for c, coro in enumerate(tasks.coroutines):
interval = coro["interval"]
worker = coro["worker"]
state = coro.get("state", dict())
state["bot"] = tasks.bot
tasks.periodic(tasks.scheduler, interval, worker, c, state)
tasks.scheduler.run()
def add_coroutine(self, worker, interval, state=dict()):
self.coroutines.append({
"worker": worker,
"interval": interval,
"state": state
})
def run(self):
self.thread.daemon = True
self.thread.start()

41
coroutines/__init__.py Normal file
View File

@ -0,0 +1,41 @@
from coroutines.bbj import BBJ
from coroutines.rss import RSS
# {
# "worker": test,
# "interval": 3
# }
# def test(bot):
# print("Testing {}".format(bot.botnick))
coroutines = [
{
"worker": lambda state: BBJ(state).start(),
"interval": 5,
"state": {
"alias": "bbj",
"source": "http://localhost:7099/api",
"channels": ["#team"]
}
},
{
"worker": lambda state: RSS(state).start(),
"interval": 6,
"state": {
"alias": "title",
"source": "https://tilde.news/newest.rss",
"use": "title",
"channels": ["#meta", "#tildeverse"]
}
},
{
"worker": lambda state: RSS(state).start(),
"interval": 8,
"state": {
"alias": "links-comments",
"source": "https://tilde.news/comments.rss",
"use": "summary",
"channels": ["#tildeverse"]
}
}
]

89
coroutines/bbj.py Normal file
View File

@ -0,0 +1,89 @@
from urllib.request import Request, urlopen
from urllib.parse import urlencode
from urllib.error import HTTPError
from datetime import datetime
from json import loads, dumps
from re import sub
class BBJ:
def __init__(self, state):
self.name = "BBJ"
self.bot = state["bot"]
self.alias = state["alias"]
self.source = state["source"]
self.channels = state["channels"]
self.memory = state.get("memory", {
"initialized": False,
"known": dict()
})
def start(self):
if not self.memory["initialized"]:
self.memory["initialized"] = True
self.fetch(self.cache)
return self.run()
def run(self):
self.fetch(self.mirror)
return {
"bot": self.bot,
"alias": self.alias,
"source": self.source,
"channels": self.channels,
"memory": self.memory
}
def cache(self, item):
self.memory["known"][item["thread_id"]] = item["last_mod"]
def process_thread(self, thread_id, thread):
data = thread.get("data", dict())
title = data.get("title", "")
replies = data.get("reply_count", "")
messages = data.get("messages", "")
usermap = thread.get("usermap", dict())
reply = messages[replies]
author = reply.get("author", "")
username = usermap[author].get("user_name", "")
body = reply.get("body", "")
body = sub(r">>\d\n\n", r"", body)
body = sub(r"\n", r" ", body)
php = "https://bbj.tilde.team/index.php"
link = "{}?thread_id={}".format(php, thread_id)
for channel in self.channels:
response = "'{}' ({}) : {} <{}>".format(title, username, body, link)
message = "[{}] {}".format(self.alias, response)
self.bot.send_message(channel, message)
def get_thread(self, thread_id, callback):
params = {
"thread_id": thread_id
}
post_params = str(dumps(params)).encode()
thread_load = Request("{}/thread_load".format(self.source), post_params)
thread_load.add_header("Content-Type", "application/json")
try:
response = callback(thread_id, loads(urlopen(thread_load).read()))
except HTTPError:
return
def mirror(self, item):
thread_id = item["thread_id"]
last_mod = self.memory["known"][thread_id]
if last_mod == item["last_mod"]:
return
self.memory["known"][thread_id] = item["last_mod"]
self.get_thread(thread_id, self.process_thread)
def fetch(self, callback):
thread_index = Request("{}/thread_index".format(self.source))
try:
response = loads(urlopen(thread_index).read())
threads = response.get("data", dict())
for thread in threads:
callback(thread)
except HTTPError:
return

69
coroutines/rss.py Normal file
View File

@ -0,0 +1,69 @@
from xml.etree import ElementTree as etree
from urllib.request import Request, urlopen
from urllib.error import HTTPError
from json import loads, dumps
from re import sub
class RSS:
def __init__(self, state):
self.name = "RSS"
self.bot = state["bot"]
self.alias = state["alias"]
self.use = state["use"]
self.source = state["source"]
self.channels = state["channels"]
self.memory = state.get("memory", {
"initialized": False,
"known": list()
})
def start(self):
if not self.memory["initialized"]:
self.memory["initialized"] = True
self.fetch(self.cache)
return self.run()
def run(self):
self.fetch(self.mirror)
return {
"bot": self.bot,
"alias": self.alias,
"use": self.use,
"source": self.source,
"channels": self.channels,
"memory": self.memory
}
def cache(self, item):
guid = item.findtext("guid", None)
if guid is not None:
self.memory["known"].append(guid)
def mirror(self, item):
guid = item.findtext("guid", None)
if guid is None:
return
if guid in self.memory["known"]:
return
self.memory["known"].append(guid)
use = sub(r"(<\/?[^>]+>)|\\n", "", item.findtext(self.use, ""))
user = item.findtext("author", "").split("@")[0]
post = "{} (posted by {}) <{}>".format(use, user, guid)
response = "[{}] {}".format(self.alias, post)
for channel in self.channels:
self.bot.send_message(channel, response)
def fetch(self, callback):
req = Request(self.source)
try:
response = urlopen(req).read()
except HTTPError:
return
feed = etree.fromstring(response)
items = feed.findall("channel/item")
for item in items:
callback(item)

0
data/.gitkeep Normal file
View File