commit
456f9cc01d
|
@ -0,0 +1,14 @@
|
|||
venv/
|
||||
json/
|
||||
users/
|
||||
__pycache__/
|
||||
doctorow_ebooks.txt
|
||||
.indexme
|
||||
*.swp
|
||||
*.log
|
||||
config.ini
|
||||
config.json
|
||||
plugins/pronouns.json
|
||||
plugins/ebooks/
|
||||
optout
|
||||
secrets.toml
|
|
@ -0,0 +1,4 @@
|
|||
# ju - juju
|
||||
This is a live bot that runs on the [pinhook framework](https://github.com/archangelic/pinhook)
|
||||
|
||||
It's prefix is "&"
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
|
||||
import pinhook.bot
|
||||
|
||||
with open('config.json') as c:
|
||||
config = json.load(c)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == '--test-mode':
|
||||
channels = ['#jmjl-dev']
|
||||
else:
|
||||
channels = config['channels']
|
||||
else:
|
||||
channels = config['channels']
|
||||
bot = pinhook.bot.Bot(
|
||||
channels,
|
||||
'ju',
|
||||
'localhost',
|
||||
ops=['jmjl'],
|
||||
ns_pass=config['password'],
|
||||
nickserv='nickserv',
|
||||
cmd_prefix='&'
|
||||
)
|
||||
bot.start()
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
import math
|
||||
import json
|
||||
import time
|
||||
|
||||
from pinhook import plugin as p
|
||||
|
||||
with open('emojis.json', 'r', encoding='utf-8') as e:
|
||||
emojis = json.load(e)
|
||||
|
||||
emojis = sorted(emojis.items(), key=lambda x: x[0])
|
||||
|
||||
emoji_list = {}
|
||||
c = 0
|
||||
for k,v in emojis:
|
||||
emoji_list[str(c)] = v
|
||||
c += 1
|
||||
|
||||
@p.command('&emojitime')
|
||||
def emojitime(msg):
|
||||
e_len = len(emoji_list)
|
||||
now = int(time.time() * 100)
|
||||
s = math.floor(now % e_len)
|
||||
s = emoji_list[str(s)]
|
||||
|
||||
m = math.floor((now % e_len**2)/e_len)
|
||||
m = emoji_list[str(m)]
|
||||
|
||||
h = math.floor((now % e_len**3)/e_len**2)
|
||||
h = emoji_list[str(h)]
|
||||
return p.message(u':'.join([h,m,s]))
|
|
@ -0,0 +1,7 @@
|
|||
from pinhook import plugin as p
|
||||
|
||||
# !rollcall
|
||||
@p.command('!rollcall', help_text="The rollcall command to introduce the bot to newcomers")
|
||||
def rollcall(msg):
|
||||
out = "Beep boop, i'm a bot from jmjl. I currently can show you the date join and leave channels, if you need more, be sure to make a pr on git~town/jmjl/juju. I also have current music but that's broken."
|
||||
return p.message(out)
|
|
@ -0,0 +1,241 @@
|
|||
from pinhook import plugin as p
|
||||
import random
|
||||
import json
|
||||
import requests
|
||||
import typing
|
||||
import datetime as _datetime
|
||||
import dateutil.relativedelta
|
||||
import enum
|
||||
|
||||
ISO8601_FORMAT_DT = "%Y-%m-%dT%H:%M:%S"
|
||||
ISO8601_FORMAT_TZ = "%z"
|
||||
|
||||
TIME_HUMAN = "%H:%M:%S"
|
||||
DATE_HUMAN = "%Y-%m-%d"
|
||||
|
||||
class TimeSpec(enum.Enum):
|
||||
NORMAL = 1
|
||||
MILLISECOND = 2
|
||||
|
||||
TIME_SECOND = 1
|
||||
TIME_MINUTE = TIME_SECOND*60
|
||||
TIME_HOUR = TIME_MINUTE*60
|
||||
TIME_DAY = TIME_HOUR*24
|
||||
TIME_WEEK = TIME_DAY*7
|
||||
|
||||
SECONDS_MINUTES = 60
|
||||
SECONDS_HOURS = SECONDS_MINUTES*60
|
||||
SECONDS_DAYS = SECONDS_HOURS*24
|
||||
SECONDS_WEEKS = SECONDS_DAYS*7
|
||||
|
||||
UNIT_MINIMUM = 6
|
||||
UNIT_SECOND = 5
|
||||
UNIT_MINUTE = 4
|
||||
UNIT_HOUR = 3
|
||||
UNIT_DAY = 2
|
||||
UNIT_WEEK = 1
|
||||
UNIT_MONTH = 1
|
||||
UNIT_YEAR = 1
|
||||
|
||||
def utcnow() -> _datetime.datetime:
|
||||
return _datetime.datetime.utcnow().replace(tzinfo=_datetime.timezone.utc)
|
||||
|
||||
def timestamp(seconds: float) -> _datetime.datetime:
|
||||
return _datetime.datetime.fromtimestamp(seconds).replace(
|
||||
tzinfo=_datetime.timezone.utc)
|
||||
|
||||
def seconds_since(dt: _datetime.datetime) -> float:
|
||||
return (utcnow()-dt).total_seconds()
|
||||
|
||||
class RelativeDirection(enum.Enum):
|
||||
FORWARD = 1
|
||||
BACKWARD = 2
|
||||
|
||||
|
||||
|
||||
LISTEN_URL = "https://tilderadio.org/listen"
|
||||
SCHEDULE_URL = "https://tilderadio.org/schedule/"
|
||||
SOURCE_URL = "https://tildegit.org/ben/bitbot-modules"
|
||||
AZURACAST_API_BASE = "https://azuracast.tilderadio.org/api"
|
||||
ICECAST_API_BASE = "https://azuracast.tilderadio.org/radio/8000"
|
||||
|
||||
now_playing = ""
|
||||
dj = ""
|
||||
song = ""
|
||||
listeners = 0
|
||||
is_online = False
|
||||
|
||||
def save_nowplaying(jsontxt):
|
||||
if jsontxt == "":
|
||||
data = requests.get(AZURACAST_API_BASE + "/nowplaying/1").json()
|
||||
else:
|
||||
data = json.loads(jsontxt)
|
||||
|
||||
# get the song name directly from icecast
|
||||
icecast_data = requests.get(ICECAST_API_BASE + "/status-json.xsl").json()
|
||||
np = icecast_data["icestats"]["source"]
|
||||
song = np[0]["yp_currently_playing"]
|
||||
listeners = sum(i["listeners"] for i in np)
|
||||
|
||||
# azuracast's now playing info is broken
|
||||
# https://github.com/AzuraCast/AzuraCast/issues/3142
|
||||
# song = data["now_playing"]["song"]["text"]
|
||||
is_online = data["live"]["is_live"]
|
||||
dj = data["live"]["streamer_name"]
|
||||
broadcast_start = data["live"]["broadcast_start"]
|
||||
# listeners = data["listeners"]["current"]
|
||||
|
||||
def format_nowplaying():
|
||||
ret = ""
|
||||
if is_online:
|
||||
ret = f"({dj}) "
|
||||
ret += f"Now playing: {song} /|\ {listeners} listeners"
|
||||
return ret
|
||||
|
||||
def on_load():
|
||||
save_nowplaying("")
|
||||
|
||||
# &np &nowplaying [Show the current song on tilderadio]
|
||||
@p.command("&np", help_text="Show the current song on tilderadio")
|
||||
def nowplaying(event):
|
||||
save_nowplaying("")
|
||||
event["stdout"].write(format_nowplaying())
|
||||
|
||||
# &schedule [Show a link to the tilderadio schedule]
|
||||
def schedule(event):
|
||||
event["stdout"].write(f"You can find the schedule here: {SCHEDULE_URL}")
|
||||
|
||||
|
||||
# &un [Show who's up next to stream (schedule)]
|
||||
@p.command("&un", help_text="Show who's up next to stream (schedule)")
|
||||
def upnext(event):
|
||||
js = requests.get(
|
||||
AZURACAST_API_BASE + "/station/1/schedule"
|
||||
).json()
|
||||
if len(js) < 1:
|
||||
event["stdout"].write("nothing scheduled")
|
||||
else:
|
||||
data = js[0]
|
||||
print(data["start"]);
|
||||
start = iso8601(data["start"])
|
||||
now = utcnow()
|
||||
total_secs = (start - now).total_seconds()
|
||||
event["stdout"].write(
|
||||
"{} is up next at {} UTC in {}!".format(
|
||||
data["name"],
|
||||
datetime_human(start),
|
||||
to_pretty_time(total_secs),
|
||||
)
|
||||
)
|
||||
|
||||
# &unn [Show who's up after the next to stream (schedule)]
|
||||
def upnextnext(event):
|
||||
js = utils.http.request(
|
||||
AZURACAST_API_BASE + "/station/1/schedule"
|
||||
).json()
|
||||
if len(js) < 1:
|
||||
event["stdout"].write("nothing scheduled")
|
||||
else:
|
||||
data = js[1]
|
||||
start = iso8601(data["start"])
|
||||
now = utils.datetime.utcnow()
|
||||
total_secs = (start - now).total_seconds()
|
||||
event["stdout"].write(
|
||||
"{} is up next next at {} UTC in {}!".format(
|
||||
data["name"],
|
||||
datetime_human(start),
|
||||
to_pretty_time(total_secs),
|
||||
)
|
||||
)
|
||||
|
||||
# &dj [View who is streaming if any]
|
||||
def showdj(event):
|
||||
if dj == "":
|
||||
message = "No one is currently on the air"
|
||||
else:
|
||||
message = f"{dj} is now streaming!"
|
||||
if broadcast_start:
|
||||
now = utils.datetime.utcnow().timestamp()
|
||||
total_seconds = now - broadcast_start
|
||||
message += " (for {})".format(
|
||||
to_pretty_time(total_seconds)
|
||||
)
|
||||
event["stdout"].write(message)
|
||||
|
||||
|
||||
# Funtions for this to work
|
||||
|
||||
def datetime_human(dt: _datetime.datetime, timespec: TimeSpec=TimeSpec.NORMAL):
|
||||
date = _datetime.datetime.strftime(DATE_HUMAN) # removed dt,
|
||||
time = _datetime.datetime.strftime(TIME_HUMAN) # removed dt,
|
||||
if timespec == TimeSpec.MILLISECOND:
|
||||
time += ".%s" % str(int(dt.microsecond/1000)).zfill(3)
|
||||
|
||||
def iso8601(dt: _datetime.datetime, timespec: TimeSpec=TimeSpec.NORMAL
|
||||
) -> str:
|
||||
dt_format = dt.strftime(ISO8601_FORMAT_DT)
|
||||
tz_format = dt.strftime(ISO8601_FORMAT_TZ)
|
||||
|
||||
ms_format = ""
|
||||
if timespec == TimeSpec.MILLISECOND:
|
||||
ms_format = ".%s" % str(int(dt.microsecond/1000)).zfill(3)
|
||||
|
||||
return "%s%s%s" % (dt_format, ms_format, tz_format)
|
||||
|
||||
def datetime_human(dt: _datetime.datetime, timespec: TimeSpec=TimeSpec.NORMAL):
|
||||
date = _datetime.datetime.strftime(DATE_HUMAN) # removed dt,
|
||||
time = _datetime.datetime.strftime(TIME_HUMAN) # removed dt,
|
||||
if timespec == TimeSpec.MILLISECOND:
|
||||
time += ".%s" % str(int(dt.microsecond/1000)).zfill(3)
|
||||
return "%s %s" % (date, time)
|
||||
def date_human(dt: _datetime.datetime, timespec: TimeSpec=TimeSpec.NORMAL):
|
||||
return _datetime.datetime.strftime(DATE_HUMAN) # removed dt,
|
||||
|
||||
def to_pretty_time(total_seconds: int, max_units: int=UNIT_MINIMUM,
|
||||
direction: typing.Optional[RelativeDirection]=None) -> str:
|
||||
if total_seconds < 1:
|
||||
return "0s"
|
||||
|
||||
if not direction == None:
|
||||
now = utcnow()
|
||||
later = now
|
||||
mod = _datetime.timedelta(seconds=total_seconds)
|
||||
if direction == RelativeDirection.FORWARD:
|
||||
later += mod
|
||||
else:
|
||||
later -= mod
|
||||
|
||||
dts = [later, now]
|
||||
relative = dateutil.relativedelta.relativedelta(max(dts), min(dts))
|
||||
years = relative.years
|
||||
months = relative.months
|
||||
weeks, days = divmod(relative.days, 7)
|
||||
hours = relative.hours
|
||||
minutes = relative.minutes
|
||||
seconds = relative.seconds
|
||||
else:
|
||||
years, months = 0, 0
|
||||
weeks, days = divmod(total_seconds, SECONDS_WEEKS)
|
||||
days, hours = divmod(days, SECONDS_DAYS)
|
||||
hours, minutes = divmod(hours, SECONDS_HOURS)
|
||||
minutes, seconds = divmod(minutes, SECONDS_MINUTES)
|
||||
|
||||
out: typing.List[str] = []
|
||||
if years and len(out) < max_units:
|
||||
out.append("%dy" % years)
|
||||
if months and len(out) < max_units:
|
||||
out.append("%dmo" % months)
|
||||
if weeks and len(out) < max_units:
|
||||
out.append("%dw" % weeks)
|
||||
if days and len(out) < max_units:
|
||||
out.append("%dd" % days)
|
||||
if hours and len(out) < max_units:
|
||||
out.append("%dh" % hours)
|
||||
if minutes and len(out) < max_units:
|
||||
out.append("%dm" % minutes)
|
||||
if seconds and len(out) < max_units:
|
||||
out.append("%ds" % seconds)
|
||||
|
||||
return " ".join(out)
|
||||
|
||||
on_load();
|
|
@ -0,0 +1,16 @@
|
|||
import subprocess
|
||||
|
||||
import pinhook.plugin
|
||||
|
||||
@pinhook.plugin.command('&date', help_text='Current date')
|
||||
def get_time(msg):
|
||||
out = subprocess.check_output(['date']).decode().strip()
|
||||
return pinhook.plugin.message(out)
|
||||
|
||||
@pinhook.plugin.command('&load', help_text='Current CPU Load')
|
||||
@pinhook.plugin.command('&uptime', help_text='Current Server Uptime')
|
||||
def uptime(msg):
|
||||
out = subprocess.check_output(['uptime']).decode().strip()
|
||||
if msg.cmd == '&load':
|
||||
out = ', '.join(out.split(',')[-3:]).strip()
|
||||
return pinhook.plugin.message(out)
|
|
@ -0,0 +1,126 @@
|
|||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import random
|
||||
from os import listdir
|
||||
import time
|
||||
|
||||
import pinhook.plugin
|
||||
|
||||
verbs = [
|
||||
'wriggles slightly',
|
||||
'sighs cutely',
|
||||
'wriggles',
|
||||
'smiles',
|
||||
]
|
||||
|
||||
class IgnoredUsers:
|
||||
def __init__(self):
|
||||
if 'nowater.txt' not in listdir():
|
||||
open('nowater.txt', 'w').close()
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
with open('nowater.txt', 'r') as w:
|
||||
u = [i.strip() for i in w.readlines() if i.strip()]
|
||||
return u
|
||||
|
||||
ignored = IgnoredUsers()
|
||||
|
||||
@pinhook.plugin.command('&botany', help_text='look up a plant to see if it has been watered (yourself by default)')
|
||||
def run(msg):
|
||||
who = msg.nick
|
||||
if msg.arg:
|
||||
nick = msg.arg
|
||||
else:
|
||||
nick = msg.nick
|
||||
|
||||
if who == nick:
|
||||
greeting = '{}: Your'.format(who)
|
||||
else:
|
||||
greeting = '{}: {}\'s'.format(who, nick)
|
||||
try:
|
||||
with open('/home/{}/.botany/{}_plant_data.json'.format(nick, nick)) as plant_json:
|
||||
plant = json.load(plant_json)
|
||||
except FileNotFoundError:
|
||||
return pinhook.plugin.message('{}: Are you sure {} has a plant in our beautiful garden?'.format(who, nick))
|
||||
try:
|
||||
with open('/home/{}/.botany/visitors.json'.format(nick)) as visitors_json:
|
||||
visitors = json.load(visitors_json)
|
||||
if visitors:
|
||||
last_visit = visitors[-1]['timestamp']
|
||||
visitor = visitors[-1]['user']
|
||||
else:
|
||||
last_visit = 0
|
||||
visitor = ''
|
||||
except FileNotFoundError:
|
||||
last_visit = 0
|
||||
visitor = ''
|
||||
|
||||
if last_visit > plant['last_watered']:
|
||||
last_watered = last_visit
|
||||
else:
|
||||
last_watered = plant['last_watered']
|
||||
visitor = nick
|
||||
|
||||
last_watered = datetime.utcfromtimestamp(last_watered)
|
||||
|
||||
water_diff = datetime.now() - last_watered
|
||||
|
||||
if plant['is_dead'] or water_diff.days >= 5:
|
||||
condolences = ['RIP', 'Press F to Pay Respects']
|
||||
return pinhook.plugin.message('{} {} is dead. {}'.format(greeting, plant['description'], random.choice(condolences)))
|
||||
elif water_diff.days == 0:
|
||||
hours = str(round(water_diff.seconds / 3600))
|
||||
water_time = ''
|
||||
if hours == '0':
|
||||
water_time = str(round(water_diff.seconds / 60)) + ' minutes'
|
||||
elif hours == '1':
|
||||
water_time = '1 hour'
|
||||
else:
|
||||
water_time = hours + ' hours'
|
||||
msg = '{} {} was watered today! (About {} ago by {})'.format(
|
||||
greeting, plant['description'], water_time, visitor)
|
||||
return pinhook.plugin.message(msg)
|
||||
elif 1 <= water_diff.days:
|
||||
days = str(water_diff.days)
|
||||
w_days = ''
|
||||
if days == '1':
|
||||
w_days = '1 day'
|
||||
else:
|
||||
w_days = days + ' days'
|
||||
hours = str(round(water_diff.seconds / 3600))
|
||||
w_hours = ''
|
||||
if hours == '0':
|
||||
w_hours = ''
|
||||
elif hours == '1':
|
||||
w_hours = ' and 1 hour'
|
||||
else:
|
||||
w_hours = ' and {} hours'.format(hours)
|
||||
msg = "{} {} hasn't been watered today! (Last watered about {}{} ago by {})".format(
|
||||
greeting, plant['description'], w_days, w_hours, visitor)
|
||||
return pinhook.plugin.message(msg)
|
||||
|
||||
@pinhook.plugin.command('&water', help_text='water a plant (yours by default)')
|
||||
def water(msg):
|
||||
if msg.arg == msg.botnick:
|
||||
return pinhook.plugin.action(random.choice(verbs))
|
||||
elif msg.arg in ignored.users:
|
||||
return None
|
||||
elif not msg.arg:
|
||||
nick = msg.nick
|
||||
else:
|
||||
nick = msg.arg
|
||||
|
||||
try:
|
||||
filename = '/home/{}/.botany/visitors.json'.format(nick)
|
||||
with open(filename) as v:
|
||||
visitors = json.load(v)
|
||||
visitors.append({'timestamp': int(time.time()), 'user': 'pinhook'})
|
||||
with open(filename, 'w') as v:
|
||||
json.dump(visitors, v, indent=2)
|
||||
with open('/home/{}/.botany/{}_plant_data.json'.format(nick, nick)) as plant_json:
|
||||
desc = json.load(plant_json)['description']
|
||||
return pinhook.plugin.action("waters {}'s {}".format(nick, desc))
|
||||
except Exception as e:
|
||||
msg.logger.error(e)
|
||||
return pinhook.plugin.message("{}: could not find plant for {}".format(msg.nick, nick))
|
|
@ -0,0 +1,11 @@
|
|||
beautifulsoup4~=4.6.0
|
||||
configobj~=5.0.6
|
||||
emoji~=0.4.5
|
||||
geopy~=1.11.0
|
||||
markovify~=0.7.1
|
||||
nltk>~3.4.5
|
||||
pinhook~=1.4.5
|
||||
python-forecastio~=1.3.5
|
||||
requests>~2.20.0
|
||||
tvdb-api~=1.10
|
||||
zalgotext~=0.2.0
|
Loading…
Reference in New Issue