mirror of
https://github.com/OPSnet/hermes.git
synced 2026-01-16 20:04:33 -05:00
git dump
This commit is contained in:
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM python:3-alpine
|
||||
|
||||
#RUN apk --no
|
||||
WORKDIR /srv/hermes
|
||||
|
||||
COPY . /srv/hermes
|
||||
|
||||
RUN pip3 install -r requirements.txt \
|
||||
&& adduser -D -h /home/hermes -s /bin/sh hermes
|
||||
|
||||
USER hermes
|
||||
|
||||
RUN mkdir /home/hermes/.hermes
|
||||
|
||||
CMD ["run_hermes", "-vv"]
|
||||
@@ -10,7 +10,7 @@ The bot is written for Python 3.
|
||||
Installation
|
||||
------------
|
||||
|
||||
To install, run the setup.py file::
|
||||
To install, run the setup.py file
|
||||
|
||||
python3 setup.py install
|
||||
|
||||
@@ -21,7 +21,7 @@ setup to create new bin files) to run the bot through the "hermes" command.
|
||||
Usage
|
||||
-----
|
||||
|
||||
Running hermes::
|
||||
Running hermes
|
||||
|
||||
bin/hermes
|
||||
|
||||
0
bin/hermes
Executable file → Normal file
0
bin/hermes
Executable file → Normal file
28
cache_view
Normal file
28
cache_view
Normal file
@@ -0,0 +1,28 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import pprint
|
||||
import sys
|
||||
import os
|
||||
|
||||
from hermes.api import GazelleAPI
|
||||
from hermes.database import GazelleDB
|
||||
from hermes.loader import load_modules
|
||||
from hermes.utils import get_git_hash, check_pid, load_config, DotDict
|
||||
from hermes.cache import Cache
|
||||
from hermes.persist import PersistentStorage
|
||||
from hermes.hermes import HERMES_DIR
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_modules()
|
||||
storage = PersistentStorage(os.path.join(HERMES_DIR, 'persist.dat'))
|
||||
cache = Cache(storage['cache'])
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
for k in sys.argv[1:]:
|
||||
print('{0}: {1}'.format(k, pprint.pformat(cache[k])))
|
||||
else:
|
||||
cache.expire()
|
||||
keys = [k for k in iter(cache)]
|
||||
for k in keys:
|
||||
print('{0}: {1}'.format(k, pprint.pformat(cache[k])))
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
nick: hermes
|
||||
name: hermes bot
|
||||
site:
|
||||
tld: apollo.rip
|
||||
url: https://apollo.rip
|
||||
tld: orpheus.network
|
||||
url: https://orpheus.network
|
||||
irc:
|
||||
host: 127.0.0.1
|
||||
port: 6667
|
||||
channels:
|
||||
apollo:
|
||||
name: apollo
|
||||
orpheus:
|
||||
name: orpheus
|
||||
announce:
|
||||
name: announce
|
||||
devs:
|
||||
name: devs
|
||||
min_level: 800
|
||||
staff:
|
||||
name: staff
|
||||
min_level: 800
|
||||
oper:
|
||||
name: hermes
|
||||
password: password
|
||||
nickserv:
|
||||
password: password
|
||||
youtube_api: key
|
||||
database:
|
||||
host: 127.0.0.1
|
||||
port: 33060
|
||||
dbname: gazelle
|
||||
username: gazelle
|
||||
password: password
|
||||
api:
|
||||
id: id
|
||||
key: password
|
||||
interview:
|
||||
class_id: 30
|
||||
min_level: 800
|
||||
speedtest_urls:
|
||||
- "(?:https?:\\/\\/)?(?:www\\.|beta\\.|legacy\\.)?speedtest\\.net\\/(?:my-)?result(?:\\/[\\w]{2})*\\/(\\d+)(?:\\.png)?"
|
||||
site: https://interview.apollo.rip
|
||||
site: https://interview.orpheus.network
|
||||
channels:
|
||||
- interview
|
||||
- interview2
|
||||
|
||||
148
hermes/api.py
Normal file
148
hermes/api.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
API (not ajax.php) interface for Gazelle to get information from the database
|
||||
without having to directly connect to it. This class and Database should be
|
||||
interchangeable in the bot and have it function just fine.
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .utils import convert
|
||||
|
||||
|
||||
class GazelleAPI(object):
|
||||
def __init__(self, site_url, api_id, api_key, cache):
|
||||
self.site_url = site_url
|
||||
self.api_id = api_id
|
||||
self.api_key = api_key
|
||||
self.api_url = urljoin(
|
||||
self.site_url,
|
||||
'api.php?aid={}&token={}'.format(api_id, api_key)
|
||||
)
|
||||
self.cache = cache
|
||||
|
||||
def get_user(self, user):
|
||||
try:
|
||||
if isinstance(user, int):
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "user",
|
||||
"user_id": user
|
||||
})
|
||||
else:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "user",
|
||||
"username": user
|
||||
})
|
||||
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
except json.decoder.JSONDecodeError:
|
||||
return None
|
||||
|
||||
def get_topic(self, topic_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "forum",
|
||||
"topic_id": topic_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_wiki(self, wiki_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "wiki",
|
||||
"wiki_id": wiki_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_request(self, request_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "request",
|
||||
"request_id": request_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_torrent(self, torrent_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "torrent",
|
||||
"req": "torrent",
|
||||
"torrent_id": torrent_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_torrent_group(self, group_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "torrent",
|
||||
"req": "group",
|
||||
"group_id": group_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_artist(self, artist_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "artist",
|
||||
"artist_id": artist_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def get_collage(self, collage_id):
|
||||
try:
|
||||
r = requests.get(self.api_url, {
|
||||
"action": "collage",
|
||||
"collage_id": collage_id
|
||||
})
|
||||
if r.status_code == requests.codes.ok:
|
||||
response = r.json()
|
||||
return convert(response['response']) if response['status'] == 200 else None
|
||||
else:
|
||||
return None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def disconnect(self):
|
||||
pass
|
||||
@@ -6,11 +6,13 @@ from datetime import datetime, timedelta
|
||||
|
||||
from .utils import DotDict
|
||||
|
||||
|
||||
class CacheObject(object):
|
||||
def __init__(self, value, expiry):
|
||||
self.value = value
|
||||
self.expiry = expiry
|
||||
|
||||
|
||||
class Cache(object):
|
||||
def __init__(self, storage=None, expiry=None):
|
||||
if storage is not None:
|
||||
@@ -41,6 +43,15 @@ class Cache(object):
|
||||
def __contains__(self, key):
|
||||
return key in self.storage
|
||||
|
||||
def keys(self):
|
||||
return self.storage.keys()
|
||||
|
||||
def items(self):
|
||||
return self.storage.items()
|
||||
|
||||
def values(self):
|
||||
return self.storage.values()
|
||||
|
||||
def store(self, key, value, expiry=None):
|
||||
if not expiry:
|
||||
expiry = self.expiry
|
||||
@@ -63,7 +74,7 @@ class Cache(object):
|
||||
self.storage.clear()
|
||||
|
||||
def expire(self):
|
||||
for key in self.storage:
|
||||
keys = [k for k in iter(self.storage)]
|
||||
for key in keys:
|
||||
if self.storage[key].expiry < datetime.now():
|
||||
del self.storage[key]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Utility module that maps the various gazelle tables to SQLAlchemy classes so that we can
|
||||
use them nicely within hermes and not have to do something dumb like escaping our inputs
|
||||
for use within DB queries (like zookeeper).
|
||||
Utility module that maps the various gazelle tables to SQLAlchemy classes so
|
||||
that we can use them nicely within hermes and not have to do something dumb
|
||||
like escaping our inputs for use within DB queries (like zookeeper).
|
||||
"""
|
||||
|
||||
from sqlalchemy import create_engine, ForeignKey, Column
|
||||
@@ -32,7 +32,7 @@ class GazelleDB(object):
|
||||
"""
|
||||
Given a username, get the User that it matches, else return None
|
||||
|
||||
:param username:
|
||||
:param username:
|
||||
:return: User that the username belongs to if one exists
|
||||
:rtype: User
|
||||
"""
|
||||
@@ -42,7 +42,7 @@ class GazelleDB(object):
|
||||
"""
|
||||
Given a topic id, get the Topic that it matches, else return None
|
||||
|
||||
:param topic_id:
|
||||
:param topic_id:
|
||||
:return: ForumTopics that topic_id belongs to if one exists
|
||||
:rtype: ForumTopics
|
||||
"""
|
||||
|
||||
263
hermes/hermes.py
263
hermes/hermes.py
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Core Hermes module which contains all of the logic for the Bot and running the proper commands
|
||||
based off what modules have been loaded and registered for the bot. The file also contains some
|
||||
utility functions that are used within the bot, those these functions may be moved elsewhere
|
||||
as appopriate.
|
||||
Core Hermes module which contains all of the logic for the Bot and running the proper
|
||||
commands based off what modules have been loaded and registered for the bot. The file
|
||||
also contains some utility functions that are used within the bot, those these
|
||||
functions may be moved elsewhere as appopriate.
|
||||
"""
|
||||
import argparse
|
||||
import locale
|
||||
@@ -17,11 +17,13 @@ import ssl
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import irc
|
||||
|
||||
from irc.bot import SingleServerIRCBot
|
||||
from irc.connection import Factory
|
||||
|
||||
from .api import GazelleAPI
|
||||
from .database import GazelleDB
|
||||
from .irc import IRCBot
|
||||
from .loader import load_modules
|
||||
from .utils import get_git_hash, check_pid, load_config, DotDict
|
||||
from .cache import Cache
|
||||
@@ -52,7 +54,7 @@ def set_verbosity(verbose=0, level=logging.INFO):
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
class Hermes(SingleServerIRCBot):
|
||||
class Hermes(IRCBot):
|
||||
def __init__(self):
|
||||
self.logger = LOGGER
|
||||
self.dir = HERMES_DIR
|
||||
@@ -87,13 +89,25 @@ class Hermes(SingleServerIRCBot):
|
||||
self.database = None
|
||||
|
||||
if 'socket' in self.config:
|
||||
self.listener = Listener(self.config['socket']['host'], self.config['socket']['port'])
|
||||
|
||||
self.database = GazelleDB(self.config.database.host,
|
||||
self.config.database.dbname,
|
||||
self.config.database.username,
|
||||
self.config.database.password)
|
||||
self.listener = Listener(
|
||||
self.config['socket']['host'],
|
||||
self.config['socket']['port']
|
||||
)
|
||||
|
||||
if 'database' in self.config:
|
||||
self.database = GazelleDB(
|
||||
self.config.database.host,
|
||||
self.config.database.dbname,
|
||||
self.config.database.username,
|
||||
self.config.database.password
|
||||
)
|
||||
elif 'api' in self.config:
|
||||
self.database = GazelleAPI(
|
||||
self.config.site.url,
|
||||
self.config.api.id,
|
||||
self.config.api.key,
|
||||
self.cache
|
||||
)
|
||||
|
||||
self.logger.info("-> Loaded DB")
|
||||
|
||||
@@ -103,7 +117,7 @@ class Hermes(SingleServerIRCBot):
|
||||
if hasattr(mod, 'setup'):
|
||||
mod.setup(self)
|
||||
self.logger.info("Loaded module: {}".format(name))
|
||||
except:
|
||||
except BaseException:
|
||||
self.logger.exception("Error Module: {}".format(name))
|
||||
|
||||
if 'ssl' in self.config.irc and self.config.irc.ssl is True:
|
||||
@@ -118,20 +132,34 @@ class Hermes(SingleServerIRCBot):
|
||||
setattr(self, attr, self._dispatch)
|
||||
self.logger.info("-> Loaded IRC")
|
||||
|
||||
def set_nick(self, connection):
|
||||
connection.send_raw('NICK {}'.format(self.nick))
|
||||
connection.send_raw('SETIDENT {} {}'.format(self.nick, self.nick))
|
||||
connection.send_raw("SETHOST {}.{}".format(self.nick, self.config.site.tld))
|
||||
if hasattr(self.config.irc, "nickserv"):
|
||||
self.logger.info("-> Identifying with NickServ")
|
||||
connection.privmsg("NickServ", "IDENTIFY {}".format(
|
||||
self.config.irc.nickserv.password)
|
||||
)
|
||||
|
||||
def on_nicknameinuse(self, connection, event):
|
||||
"""
|
||||
Executed if someone else has already taken the bot's nickname and we cannot take it
|
||||
back via NickServ. We consider this a fatal error as this shouldn't happen in normal
|
||||
usage and would happen if we tried to run the bot twice (which is unnecessary).
|
||||
Executed if someone else has already taken the bot's nickname and we cannot
|
||||
take it back via NickServ. Kill the offending user, and take the nick
|
||||
through blood.
|
||||
|
||||
:raises: SystemError
|
||||
"""
|
||||
raise SystemError("*** ERROR: Bot's nickname in use! ***")
|
||||
self.logger.info("-> killing user named {}".format(self.nick))
|
||||
connection.kill(self.nick)
|
||||
self.set_nick(connection)
|
||||
# raise SystemError("*** ERROR: Bot's nickname in use! ***")
|
||||
|
||||
def on_erroneusenickname(self, connection, event):
|
||||
"""
|
||||
Executed if the nickname contains illegal characters (such as #) which IRC does not
|
||||
support. This is considered a fatal error and should only happen on poor configuration.
|
||||
Executed if the nickname contains illegal characters (such as #) which IRC does
|
||||
not support. This is considered a fatal error and should only happen on poor
|
||||
configuration.
|
||||
|
||||
:raises: SystemError
|
||||
"""
|
||||
@@ -139,9 +167,9 @@ class Hermes(SingleServerIRCBot):
|
||||
|
||||
def on_welcome(self, connection, event):
|
||||
"""
|
||||
Executed when the bot connects to the server (and gets the "welcome message"). We use
|
||||
this to do some initialization routines (like joining the necessary channels, etc.) that
|
||||
the bot needs to operate
|
||||
Executed when the bot connects to the server (and gets the "welcome message").
|
||||
We use this to do some initialization routines (like joining the necessary
|
||||
channels, etc.) that the bot needs to operate
|
||||
|
||||
:param connection:
|
||||
:param event:
|
||||
@@ -153,19 +181,15 @@ class Hermes(SingleServerIRCBot):
|
||||
self.logger.info("-> Setting OPER")
|
||||
connection.send_raw("OPER {} {}".format(self.config.irc.oper.name,
|
||||
self.config.irc.oper.password))
|
||||
connection.send_raw('NICK {}'.format(self.nick))
|
||||
connection.send_raw('SETIDENT {} {}'.format(self.nick, self.nick))
|
||||
if hasattr(self.config.irc, "nickserv"):
|
||||
self.logger.info("-> Identifying with NickServ")
|
||||
connection.privmsg("NickServ", "IDENTIFY {}".format(self.config.irc.nickserv.password))
|
||||
|
||||
connection.send_raw("SETHOST {}.{}".format(self.nick, self.config.site.tld))
|
||||
if self.listener is not None and self.listener.is_alive() == False:
|
||||
self.set_nick(connection)
|
||||
|
||||
if self.listener is not None and not self.listener.is_alive():
|
||||
self.listener.set_connection(connection)
|
||||
self.listener.start()
|
||||
if hasattr(self.config.irc, "channels") and isinstance(self.config.irc.channels, dict):
|
||||
if hasattr(self.config.irc, "channels") and \
|
||||
isinstance(self.config.irc.channels, dict):
|
||||
for name in self.config.irc.channels:
|
||||
channel = self.config.irc.channels[name]
|
||||
self.logger.info("-> Entering {}".format(name))
|
||||
connection.send_raw("SAJOIN {} #{}".format(self.nick, name))
|
||||
|
||||
@@ -186,9 +210,10 @@ class Hermes(SingleServerIRCBot):
|
||||
func(self, connection, event, match)
|
||||
|
||||
def check_admin(self, event):
|
||||
return event.source.nick in self.config.admins and event.source.host is not None and \
|
||||
event.source.host.endswith(self.config.site.tld) and \
|
||||
event.source.host.split(",")[0] not in self.config.admins
|
||||
return event.source.nick in self.config.admins \
|
||||
and event.source.host is not None \
|
||||
and event.source.host.endswith(self.config.site.tld) \
|
||||
and event.source.host.split(",")[0] not in self.config.admins
|
||||
|
||||
def _dispatch(self, connection, event):
|
||||
"""
|
||||
@@ -196,11 +221,12 @@ class Hermes(SingleServerIRCBot):
|
||||
:param event: class that contains that describes the IRC event
|
||||
type (type of event, always privmsg)
|
||||
source (name of who sent the message containing host and nick)
|
||||
nick -
|
||||
user -
|
||||
host -
|
||||
nick -
|
||||
user -
|
||||
host -
|
||||
target (name of who is recieving the message, in this case the bot name)
|
||||
arguments (list of arguments to the event, for this, [0] is message that was sent)
|
||||
arguments (list of arguments to the event, for this, [0] is message that
|
||||
was sent)
|
||||
tags (empty list)
|
||||
"""
|
||||
event.msg = event.arguments[0]
|
||||
@@ -219,13 +245,17 @@ class Hermes(SingleServerIRCBot):
|
||||
try:
|
||||
if event.type in func.events:
|
||||
self._execute_function(func, connection, event)
|
||||
except:
|
||||
except BaseException:
|
||||
if event.type == "privmsg":
|
||||
msg = "I'm sorry, {}.{} threw an exception. Please tell an " \
|
||||
"administrator and try again later.".format(name, func.__name__)
|
||||
msg = "I'm sorry, {}.{} threw an exception.".format(
|
||||
name,
|
||||
func.__name__
|
||||
)
|
||||
msg += " Please tell an administrator and try again later."
|
||||
connection.privmsg(event.source.nick, msg)
|
||||
self.logger.exception("Failed to run function: {}.{}".format(name,
|
||||
func.__name__))
|
||||
self.logger.exception(
|
||||
"Failed to run function: {}.{}".format(name, func.__name__)
|
||||
)
|
||||
|
||||
def disconnect(self, msg="I'll be back!"):
|
||||
if self.database is not None:
|
||||
@@ -261,27 +291,34 @@ class BotCheck(threading.Thread):
|
||||
self.alive = False
|
||||
self.join()
|
||||
|
||||
|
||||
class SaveData(threading.Thread):
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.alive = True
|
||||
self.bot = bot
|
||||
self.logger = LOGGER
|
||||
|
||||
def run(self):
|
||||
time.sleep(120)
|
||||
cycle = 480
|
||||
while self.alive:
|
||||
self.bot.storage.save()
|
||||
time.sleep(600)
|
||||
if cycle >= 600:
|
||||
self.logger.info('saving data')
|
||||
self.bot.storage.save()
|
||||
cycle = 0
|
||||
cycle += 1
|
||||
time.sleep(1)
|
||||
|
||||
def stop(self):
|
||||
self.alive = False
|
||||
self.join()
|
||||
|
||||
|
||||
class Listener(threading.Thread):
|
||||
"""
|
||||
Gazelle communicates with the IRC bot through a socket. Gazelle will send things like
|
||||
new torrents (via announce) or reports/errors that the bot would then properly relay
|
||||
into the appropriate IRC channels.
|
||||
Gazelle communicates with the IRC bot through a socket. Gazelle will send things
|
||||
like new torrents (via announce) or reports/errors that the bot would then properly
|
||||
relay into the appropriate IRC channels.
|
||||
"""
|
||||
def __init__(self, host, port):
|
||||
self.logger = LOGGER
|
||||
@@ -306,26 +343,44 @@ class Listener(threading.Thread):
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.bind((self.host, self.port))
|
||||
server_socket.listen(5)
|
||||
self.logger.info("-> Listener waiting for connection on port {}".format(self.port))
|
||||
self.logger.info(
|
||||
"-> Listener waiting for connection on port {}".format(self.port)
|
||||
)
|
||||
while self.running:
|
||||
if self.restart:
|
||||
server_socket.send("RESTARTING")
|
||||
server_socket.close()
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket = socket.socket(
|
||||
socket.AF_INET,
|
||||
socket.SOCK_STREAM
|
||||
)
|
||||
server_socket.bind((self.host, self.port))
|
||||
server_socket.listen(5)
|
||||
client_socket, address = server_socket.accept()
|
||||
data = client_socket.recv(512).decode('utf-8').strip()
|
||||
# Only accept 510 bytes as irc module appends b'\r\n' to bring
|
||||
# us to max of 512
|
||||
data = client_socket.recv(510).decode('utf-8', errors='replace').strip()
|
||||
self.logger.info("-> Listener Recieved: {}".format(data))
|
||||
client_socket.close()
|
||||
try:
|
||||
data_details = data.split()
|
||||
if data_details[0] in ["/privmsg", "privmsg"] and data_details[1] == "#":
|
||||
if len(data_details) < 2:
|
||||
continue
|
||||
if data_details[0] in ["/privmsg", "privmsg"] \
|
||||
and data_details[1] == "#":
|
||||
continue
|
||||
if self.connection is not None:
|
||||
self.connection.send_raw(data)
|
||||
except socket.error as e:
|
||||
self.logger.error("*** Socket Error: %d: %s ***" % (e.args[0], e.args[1]))
|
||||
self.logger.error(
|
||||
"*** Socket Error: %d: %s ***" % (e.args[0], e.args[1])
|
||||
)
|
||||
except irc.client.MessageTooLong:
|
||||
self.logger.warn("-> Skipping input as too long: {}".format(data))
|
||||
except irc.client.InvalidCharacters:
|
||||
self.logger.warn(
|
||||
"-> Skipping message as contained newlines: {}".format(data)
|
||||
)
|
||||
server_socket.close()
|
||||
|
||||
|
||||
@@ -338,28 +393,44 @@ def get_version_string():
|
||||
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser(description="CLI for the hermes IRC bot for Gazelle")
|
||||
parser.add_argument("-v", "--verbose", action='count', default=0,
|
||||
help="Define how much logging to do. -v will log to file, -vv will also"
|
||||
"log to sys.stdout")
|
||||
parser.add_argument("--log-level", action='store', choices=['debug', 'info', 'warn', 'error'],
|
||||
default='info',
|
||||
help="What level of messages should be logged by hermes. Most "
|
||||
"logged messages are either INFO or ERROR.")
|
||||
parser.add_argument("-V", action='version',
|
||||
version="%(prog)s ({})".format(get_version_string()))
|
||||
parser.add_argument("--nofork", action="store_true", default=False,
|
||||
help="Don't hermes as a forked daemon, as you'd like to interact with"
|
||||
"the terminal it lives in in some way. This is automatically turned "
|
||||
"on if you pass in the flag -vv.")
|
||||
parser.add_argument("--no-eternal", action="store_true", default=False,
|
||||
help="By default, the bot will attempt to restart itself after a crash"
|
||||
"(assuming the last one was not 5 seconds ago), but use this flag"
|
||||
"if you want the bot to die immediately.")
|
||||
parser.add_argument("--stop", action='store_true', default=False,
|
||||
help='Try and have a previous instance of Hermes gracefully stop')
|
||||
parser.add_argument("--kill", action='store_true', default=False,
|
||||
help="Try and have a previous intance of Hermes exit immediately.")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="CLI for the hermes IRC bot for Gazelle"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action='count', default=0,
|
||||
help="Define how much logging to do. (-v to file, -vv to stdout)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--log-level",
|
||||
action='store', choices=['debug', 'info', 'warn', 'error'], default='info',
|
||||
help="What level of messages should be logged by hermes."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-V", "--version",
|
||||
action='version',
|
||||
version="%(prog)s ({})".format(get_version_string())
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nofork",
|
||||
action="store_true", default=False,
|
||||
help="Don't run as forked daemon. (set if using -vv)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-eternal",
|
||||
action="store_true", default=False,
|
||||
help="No not attempt to restart bot in case of crash"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stop",
|
||||
action='store_true', default=False,
|
||||
help='Try and have a previous instance of Hermes gracefully stop'
|
||||
)
|
||||
parser.add_argument(
|
||||
"--kill",
|
||||
action='store_true', default=False,
|
||||
help="Try and have a previous intance of Hermes exit immediately."
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -391,30 +462,36 @@ def run_hermes():
|
||||
print("Killing instance of Hermes ({})".format(old_pid))
|
||||
os.kill(old_pid, signal.SIGKILL)
|
||||
else:
|
||||
raise SystemExit("{} already exists, exiting".format(pidfile))
|
||||
os.unlink(pidfile)
|
||||
raise SystemExit()
|
||||
raise SystemExit(
|
||||
"{} already exists, exiting".format(pidfile)
|
||||
)
|
||||
if os.path.isfile(pidfile):
|
||||
os.unlink(pidfile)
|
||||
raise SystemExit(0)
|
||||
elif args.stop or args.kill:
|
||||
raise SystemExit("Hermes is not currently running.")
|
||||
|
||||
# If we have set -vv, then we will not run as a daemon as we'll assume you wanted
|
||||
# If we have set -vv, then we will not run as a daemon
|
||||
# as we'll assume you wanted
|
||||
# to see the console output.
|
||||
if args.nofork is not True and args.verbose < 2:
|
||||
child_pid = os.fork()
|
||||
if child_pid is not 0:
|
||||
if child_pid != 0:
|
||||
raise SystemExit
|
||||
|
||||
with open(pidfile, 'w') as open_pidfile:
|
||||
open_pidfile.write(str(os.getpid()))
|
||||
|
||||
last_run = None
|
||||
irc.client.ServerConnection.buffer_class.errors = 'replace'
|
||||
|
||||
last_run = None
|
||||
save_thread = None
|
||||
try:
|
||||
hermes = Hermes()
|
||||
save_thread = SaveData(hermes)
|
||||
save_thread.start()
|
||||
#thread = BotCheck(hermes)
|
||||
#thread.start()
|
||||
# thread = BotCheck(hermes)
|
||||
# thread.start()
|
||||
|
||||
def signal_handler(sig, _):
|
||||
if sig is signal.SIGTERM:
|
||||
@@ -423,7 +500,7 @@ def run_hermes():
|
||||
hermes.die()
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
#signal.signal(signal.SIGKILL, signal_handler)
|
||||
# signal.signal(signal.SIGKILL, signal_handler)
|
||||
while run_eternal:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
@@ -433,16 +510,15 @@ def run_hermes():
|
||||
LOGGER.info("-> {}".format(e))
|
||||
hermes.disconnect("Leaving...")
|
||||
LOGGER.info("Quitting bot")
|
||||
#thread.stop()
|
||||
raise SystemExit
|
||||
break
|
||||
except RestartException:
|
||||
#thread.stop()
|
||||
# thread.stop()
|
||||
time.sleep(5)
|
||||
hermes = Hermes()
|
||||
#thread = BotCheck(hermes)
|
||||
#thread.start()
|
||||
# thread = BotCheck(hermes)
|
||||
# thread.start()
|
||||
hermes.start()
|
||||
except:
|
||||
except BaseException:
|
||||
if last_run > time.time() - 5:
|
||||
hermes.disconnect("Crashed, going offline.")
|
||||
run_eternal = False
|
||||
@@ -451,4 +527,7 @@ def run_hermes():
|
||||
time.sleep(2)
|
||||
LOGGER.exception("Crash")
|
||||
finally:
|
||||
os.unlink(pidfile)
|
||||
if save_thread is not None:
|
||||
save_thread.stop()
|
||||
if os.path.isfile(pidfile):
|
||||
os.unlink(pidfile)
|
||||
|
||||
75
hermes/irc.py
Normal file
75
hermes/irc.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import irc.bot
|
||||
import irc.client
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class ServerConnection(irc.client.ServerConnection):
|
||||
def __init__(self, reactor):
|
||||
super(ServerConnection, self).__init__(reactor)
|
||||
self.last_ping = None
|
||||
self.last_pong = None
|
||||
|
||||
def ping(self, target, target2=""):
|
||||
"""Send a PING command."""
|
||||
if not self.is_connected():
|
||||
return
|
||||
self.last_ping = datetime.now()
|
||||
self.send_items('PING', target, target2)
|
||||
|
||||
def kill(self, nick, comment=""):
|
||||
"""Send a KILL command."""
|
||||
self.send_items('KILL', nick, comment and ':' + comment)
|
||||
|
||||
|
||||
class Reactor(irc.client.Reactor):
|
||||
connection_class = ServerConnection
|
||||
|
||||
|
||||
class IRCBot(irc.bot.SingleServerIRCBot):
|
||||
reactor_class = Reactor
|
||||
timeout_interval = 40
|
||||
check_keepalive_interval = 20
|
||||
keepalive_interval = 10
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
server_list,
|
||||
nickname,
|
||||
realname,
|
||||
recon=None,
|
||||
**connect_params
|
||||
):
|
||||
if recon is None:
|
||||
recon = irc.bot.ExponentialBackoff(min_interval=10, max_interval=300)
|
||||
|
||||
super(IRCBot, self).__init__(
|
||||
server_list,
|
||||
nickname,
|
||||
realname,
|
||||
irc.bot.missing,
|
||||
recon,
|
||||
**connect_params
|
||||
)
|
||||
|
||||
for i in ['welcome', 'pong']:
|
||||
self.connection.add_global_handler(i, getattr(self, '_on_' + i), -20)
|
||||
|
||||
self.reactor.scheduler.execute_every(
|
||||
timedelta(seconds=self.check_keepalive_interval),
|
||||
self.check_keepalive
|
||||
)
|
||||
|
||||
def check_keepalive(self):
|
||||
if self.connection.last_pong is None or not self.connection.is_connected():
|
||||
return
|
||||
timeout = self.connection.last_pong + timedelta(seconds=self.timeout_interval)
|
||||
if self.connection.last_ping > timeout:
|
||||
self.connection.last_pong = None
|
||||
self.disconnect('disconnecting...')
|
||||
|
||||
def _on_welcome(self, connection, event):
|
||||
period = timedelta(seconds=self.keepalive_interval)
|
||||
connection.set_keepalive(period)
|
||||
|
||||
def _on_pong(self, connection, event):
|
||||
self.connection.last_pong = datetime.now()
|
||||
@@ -1,30 +0,0 @@
|
||||
from re import IGNORECASE
|
||||
from hermes.module import rule, event, disabled
|
||||
|
||||
|
||||
@disabled()
|
||||
@event("privmsg", "pubmsg")
|
||||
@rule(r"(https|http):\/\/(apollo|xanax)\.rip\/forums\.php\?([a-zA-Z0-9=&]*)threadid=([0-9]+)",
|
||||
IGNORECASE)
|
||||
def parse_thread_url(bot, connection, event, match):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:param match:
|
||||
:return:
|
||||
"""
|
||||
topic = bot.database.get_topic(int(match.group(4)))
|
||||
if event.type == "privmsg":
|
||||
target = event.source.nick
|
||||
else:
|
||||
target = event.target
|
||||
if topic is None:
|
||||
msg = "Could not find topic"
|
||||
else:
|
||||
msg = "[ {} | " \
|
||||
"https://apollo.rip/forums.php?action=showthread&threadid={} ]".format(topic.title,
|
||||
topic.id)
|
||||
connection.privmsg(target, msg)
|
||||
@@ -54,10 +54,13 @@ def update_bot(bot, connection, event):
|
||||
if err is not None and str(err, "utf-8") != "":
|
||||
bot.logger.error(str(err, "utf-8"))
|
||||
os.chdir(current_dir)
|
||||
get_version(bot,connection, event)
|
||||
#restart_bot(bot, connection, event)
|
||||
get_version(bot, connection, event)
|
||||
# restart_bot(bot, connection, event)
|
||||
else:
|
||||
connection.privmsg(event.source.nick, "Can only update bot if run from git directory.")
|
||||
connection.privmsg(
|
||||
event.source.nick,
|
||||
"Can only update bot if run from git directory."
|
||||
)
|
||||
|
||||
|
||||
@admin_only()
|
||||
@@ -72,7 +75,10 @@ def kill_bot(bot, *_):
|
||||
@privmsg()
|
||||
@command("version")
|
||||
def get_version(_, connection, event):
|
||||
connection.privmsg(event.source.nick, "Running version: {}".format(get_version_string()))
|
||||
connection.privmsg(
|
||||
event.source.nick,
|
||||
"Running version: {}".format(get_version_string())
|
||||
)
|
||||
|
||||
|
||||
@admin_only()
|
||||
@@ -82,7 +88,7 @@ def view_log(bot, connection, event):
|
||||
log_file = os.path.join(bot.dir, 'hermes.log')
|
||||
try:
|
||||
connection.privmsg(event.source.nick, "Log file: {}".format(log_file))
|
||||
for line in file_tail(log_file, 10):
|
||||
connection.privmsg(event.source.nick, line)
|
||||
for line in file_tail(log_file, 30):
|
||||
connection.privmsg(event.source.nick, line.strip())
|
||||
except Exception as e:
|
||||
connection.privmsg(event.source.nick, e)
|
||||
|
||||
@@ -8,13 +8,15 @@ Note, because the bot is using SAJOIN to add users to the channels that they req
|
||||
we cannot use a +b ban on a user to prevent them from joining a channel. Instead, we have to
|
||||
revoke their IRC privileges through Gazelle, then kick the user from the channels.
|
||||
"""
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from hermes.module import privmsg, command, help_message, example
|
||||
from datetime import timedelta, datetime
|
||||
from hermes.module import privmsg, command, help_message, example, admin_only
|
||||
|
||||
|
||||
timeouts = {}
|
||||
|
||||
|
||||
def validate_irckey(user, irckey):
|
||||
if user == None:
|
||||
if user is None:
|
||||
return False, "No user found with that name."
|
||||
else:
|
||||
if user.Enabled != '1' or user.DisableIRC == '1':
|
||||
@@ -24,22 +26,24 @@ def validate_irckey(user, irckey):
|
||||
else:
|
||||
return False, "Invalid Username/IRC Key"
|
||||
|
||||
|
||||
@privmsg()
|
||||
@command("enter")
|
||||
@help_message("Use this command to have the bot add you to any official channel")
|
||||
@example("enter <site_username> <site_irckey> <channels>",
|
||||
"enter #APOLLO itismadness 123456",
|
||||
"enter #APOLLO #announce itismadness 123456",
|
||||
"enter #APOLLO,#announce itismadness 123456")
|
||||
@example("enter <channels> <site_username> <site_irckey>",
|
||||
"enter #orpheus itismadness 123456",
|
||||
"enter #orpheus #announce itismadness 123456",
|
||||
"enter #orpheus,#announce itismadness 123456")
|
||||
def enter(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
global timeouts
|
||||
|
||||
sent_nick = event.source.nick
|
||||
if len(event.args) < 3:
|
||||
@@ -48,25 +52,41 @@ def enter(bot, connection, event):
|
||||
return
|
||||
username = event.args[-2]
|
||||
password = event.args[-1]
|
||||
|
||||
if username.lower() in timeouts:
|
||||
if (datetime.now() - timeouts[username.lower()]) > timedelta(hours=24):
|
||||
del timeouts[username.lower()]
|
||||
else:
|
||||
connection.privmsg("You are still on timeout, please come back later")
|
||||
return
|
||||
|
||||
channels = []
|
||||
for channel in event.args[:-2]:
|
||||
channels.extend([chan.strip() for chan in channel.strip().split(",")])
|
||||
bot.logger.debug("-> {} (username: {}) wants to enter {}".format(sent_nick, username,
|
||||
", ".join(channels)))
|
||||
bot.logger.debug(
|
||||
"-> {} (username: {}) wants to enter {}".format(
|
||||
sent_nick,
|
||||
username,
|
||||
", ".join(channels)
|
||||
)
|
||||
)
|
||||
|
||||
# Pull fresh copy of user, use the cached version if no user is found
|
||||
key = "user_{0}".format(username)
|
||||
user = bot.database.get_user(username)
|
||||
if user == None:
|
||||
if user is None:
|
||||
user = bot.cache[key]
|
||||
valid, error = validate_irckey(user, password)
|
||||
|
||||
if valid:
|
||||
bot.cache.store(key, user, timedelta(30))
|
||||
connection.send_raw("CHGIDENT {} {}".format(sent_nick, user.ID))
|
||||
connection.send_raw("CHGHOST {} {}.{}.{}".format(sent_nick, user.Username,
|
||||
user.ClassName.replace(" ", ""),
|
||||
bot.config.site.tld))
|
||||
connection.send_raw("CHGHOST {} {}.{}.{}".format(
|
||||
sent_nick,
|
||||
user.Username,
|
||||
user.ClassName.replace(" ", ""),
|
||||
bot.config.site.tld
|
||||
))
|
||||
joined = []
|
||||
not_real = []
|
||||
not_joined = []
|
||||
@@ -117,3 +137,15 @@ def enter(bot, connection, event):
|
||||
bot.logger.debug("-> {} entered #{}".format(sent_nick, ", #".join(joined)))
|
||||
else:
|
||||
connection.privmsg(sent_nick, error)
|
||||
|
||||
|
||||
@privmsg()
|
||||
@admin_only()
|
||||
@command("timeout")
|
||||
def timeout(bot, connection, event):
|
||||
global timeouts
|
||||
|
||||
client_name = event.args[-2]
|
||||
site_name = event.args[-1]
|
||||
connection.send_raw("KILL {} Been placed on a 1 day timeout".format(client_name))
|
||||
timeouts[site_name.lower()] = datetime.now()
|
||||
|
||||
@@ -15,10 +15,10 @@ def setup(bot):
|
||||
|
||||
|
||||
@event("pubmsg", "privmsg")
|
||||
@rule(r'[!\.][a-zA-Z0-9]+')
|
||||
@rule(r'^[!\.][a-zA-Z0-9]+')
|
||||
def can_trigger(bot, connection, event, match):
|
||||
trigger = event.cmd.lower().strip('!.')
|
||||
target = event.source.nick if event.type == "privmsg" else event.target
|
||||
target = event.source.nick if event.type == 'privmsg' else event.target
|
||||
if trigger in bot.storage[key]:
|
||||
connection.privmsg(target, bot.storage[key][trigger])
|
||||
|
||||
@@ -61,16 +61,13 @@ def can_add(bot, connection, event, args):
|
||||
trigger = None if args is None or len(args) == 0 else args[0]
|
||||
message = None if args is None or len(args) < 2 else args[1:]
|
||||
if trigger is None or message is None:
|
||||
connection.notice(event.source.nick, "Please specify a trigger and \
|
||||
message.")
|
||||
connection.notice(event.source.nick, "Please specify a trigger and message.")
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
connection.privmsg(event.source.nick, "Trigger {0} updated.".format(
|
||||
trigger))
|
||||
connection.notice(event.source.nick, "Trigger {0} updated.".format(trigger))
|
||||
else:
|
||||
connection.privmsg(event.source.nick, "Trigger {0} added.".format(
|
||||
trigger))
|
||||
connection.notice(event.source.nick, "Trigger {0} added.".format(trigger))
|
||||
|
||||
bot.storage[key][trigger] = " ".join(message)
|
||||
|
||||
@@ -82,12 +79,10 @@ def can_del(bot, connection, event, args):
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
connection.notice(event.source.nick, "Trigger {0} deleted.".format(
|
||||
trigger))
|
||||
connection.notice(event.source.nick, "Trigger {0} deleted.".format(trigger))
|
||||
del bot.storage[key][trigger]
|
||||
else:
|
||||
connection.notice(event.source.nick, "Couldn't find trigger {0}.".format(
|
||||
trigger))
|
||||
connection.notice(event.source.nick, "Couldn't find trigger {0}.".format(trigger))
|
||||
|
||||
|
||||
def can_list(bot, connection, event, args):
|
||||
@@ -107,7 +102,8 @@ def check_auth(bot, connection, host, nick, prompt):
|
||||
user = bot.database.get_user(split_host[0])
|
||||
if user is None:
|
||||
if prompt:
|
||||
connection.notice(nick, "You must be authed through the bot to administer canned responses.")
|
||||
connection.notice(nick, "You must be authed through the bot to administer \
|
||||
canned responses.")
|
||||
return False
|
||||
|
||||
if bot.config.fls.class_id in user['SecondaryClasses'] or \
|
||||
|
||||
@@ -6,11 +6,14 @@ from hermes.module import event, command, disabled, admin_only
|
||||
from time import time
|
||||
import re
|
||||
|
||||
key = "interview_queue"
|
||||
key = 'interview_queue'
|
||||
speedtest_key = 'speedtest_history'
|
||||
|
||||
def setup(bot):
|
||||
if bot.storage[key] is None:
|
||||
bot.storage[key] = list()
|
||||
if bot.storage[speedtest_key] is None:
|
||||
bot.storage[speedtest_key] = list()
|
||||
|
||||
|
||||
def convert_time(diff):
|
||||
@@ -31,11 +34,11 @@ class UserClass:
|
||||
self.time = time()
|
||||
self.speed_test = speed_test
|
||||
self.postponed = False
|
||||
|
||||
|
||||
def get_waited(self):
|
||||
waited = time() - self.time
|
||||
return convert_time(waited)
|
||||
|
||||
|
||||
def get_waited_str(self):
|
||||
days, hours, minutes, seconds = self.get_waited()
|
||||
return "{} days, {} hours, {} minutes, and " \
|
||||
@@ -56,6 +59,10 @@ def is_in_queue(bot, user, host):
|
||||
return is_already_in_queue, position
|
||||
|
||||
|
||||
def is_url_reused(bot, url):
|
||||
return url in bot.storage[speedtest_key]
|
||||
|
||||
|
||||
def check_auth(bot, connection, host, nick, prompt):
|
||||
split_host = host.split(".")
|
||||
if len(split_host) != 4:
|
||||
@@ -140,6 +147,11 @@ def queue(bot, connection, event):
|
||||
connection.notice(nick, "You are already in the queue at position "
|
||||
"{}.".format(str(position + 1)))
|
||||
else:
|
||||
if is_url_reused(bot, speed_test):
|
||||
connection.notice(nick, "Speedtest URL has already been used, take another")
|
||||
return
|
||||
|
||||
bot.storage[speedtest_key].append(speed_test)
|
||||
bot.storage[key].append(UserClass(nick, host, user, speed_test))
|
||||
connection.notice(nick, "Successfully added to queue. You are at position "
|
||||
"{}.".format(str(len(bot.storage[key]))))
|
||||
@@ -222,9 +234,9 @@ def postpone(bot, connection, event):
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
:return:
|
||||
"""
|
||||
nick = event.source.nick
|
||||
user = event.source.user
|
||||
@@ -251,9 +263,9 @@ def cancel(bot, connection, event):
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
:return:
|
||||
"""
|
||||
|
||||
nick = event.source.nick
|
||||
|
||||
112
hermes/modules/orpheus.py
Normal file
112
hermes/modules/orpheus.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from re import IGNORECASE
|
||||
from hermes.module import rule, event, disabled
|
||||
|
||||
def check_perms(bot, channel, level):
|
||||
channel = channel.lstrip('#').lower()
|
||||
if channel not in bot.config.irc.channels:
|
||||
return False
|
||||
config_channel = bot.config.irc.channels[channel]
|
||||
if 'min_level' not in config_channel or level > config_channel.min_level or (
|
||||
'public' in config_channel and config_channel.public == True):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/forums\.php\?[a-zA-Z0-9=&]*threadid=([0-9]+)", IGNORECASE)
|
||||
def parse_thread_url(bot, connection, event, match):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:param match:
|
||||
:return:
|
||||
"""
|
||||
topic = bot.database.get_topic(int(match.group(1)))
|
||||
if topic is not None:
|
||||
if not check_perms(bot, event.target, topic.MinClassRead):
|
||||
return
|
||||
|
||||
msg = "[ Forums :: {0} | {1} ]".format(topic.Forum, topic.Title)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/wiki\.php\?[a-zA-Z0-9=&]*id=([0-9]+)")
|
||||
def parse_wiki_url(bot, connection, event, match):
|
||||
wiki = bot.database.get_wiki(int(match.group(1)))
|
||||
if wiki is not None:
|
||||
if not check_perms(bot, event.target, wiki.MinClassRead):
|
||||
return
|
||||
|
||||
msg = "[ Wiki :: {0} ]".format(wiki.Title)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/user\.php\?[a-zA-Z0-9=&]*id=([0-9]+)")
|
||||
def parse_user_url(bot, connection, event, match):
|
||||
user = bot.database.get_user(int(match.group(1)))
|
||||
if user is not None:
|
||||
msg = "[ {0} ] :: [ {1} ] :: [ Uploaded: {2} | Downloaded: {3} | " \
|
||||
"Ratio: {4} ]".format(user.Username, user.ClassName, user.DisplayStats.Uploaded,
|
||||
user.DisplayStats.Downloaded, user.DisplayStats.Ratio)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/requests\.php\?[a-zA-Z0-9=&]*id=([0-9]+)")
|
||||
def parse_request_url(bot, connection, event, match):
|
||||
request = bot.database.get_request(int(match.group(1)))
|
||||
if request is not None:
|
||||
msg = "[ Request :: {0} - {1} ({2}) ]".format(request.DisplayArtists, request.Title,
|
||||
request.Year)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/torrents\.php\?[a-zA-Z0-9=&]*torrentid=([0-9]+)")
|
||||
def parse_torrent_url(bot, connection, event, match):
|
||||
torrent = bot.database.get_torrent(int(match.group(1)))
|
||||
if torrent is not None:
|
||||
if torrent.HasLogDB == "1":
|
||||
log = " {0}%".format(torrent.LogScore)
|
||||
elif torrent.HasLog == "1":
|
||||
log = " Log"
|
||||
else:
|
||||
log = ""
|
||||
|
||||
msg = "[ Torrent :: {0} - {1} ({2}) [{3}] | {4} {5}{6} ]".format(torrent.DisplayArtists,
|
||||
torrent.Name, torrent.Year, torrent.ReleaseType, torrent.Media, torrent.Format,
|
||||
log)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/torrents\.php[a-zA-Z0-9=&\?]*[\?&]id=([0-9]+)$")
|
||||
def parse_torrent_group_url(bot, connection, event, match):
|
||||
group = bot.database.get_torrent_group(int(match.group(1)))
|
||||
if group is not None:
|
||||
msg = "[ Torrent :: {0} - {1} ({2}) [{3}] ]".format(group.DisplayArtists, group.Name,
|
||||
group.Year, group.ReleaseType)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/artist\.php\?[a-zA-Z0-9=&]*id=([0-9]+)")
|
||||
def parse_artist_url(bot, connection, event, match):
|
||||
artist = bot.database.get_artist(int(match.group(1)))
|
||||
if artist is not None:
|
||||
msg = "[ Artist :: {0} ]".format(artist.Name)
|
||||
connection.privmsg(event.target, msg)
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@rule(r"https:\/\/orpheus\.network\/collages\.php\?[a-zA-Z0-9=&]*id=([0-9]+)")
|
||||
def parse_collage_url(bot, connection, event, match):
|
||||
collage = bot.database.get_collage(int(match.group(1)))
|
||||
if collage is not None:
|
||||
msg = "[ Collage :: {0} [{1}] ]".format(collage.Name, collage.Category)
|
||||
connection.privmsg(event.target, msg)
|
||||
110
hermes/modules/quotes.py
Normal file
110
hermes/modules/quotes.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Module to provide randomised quotes.
|
||||
Administration is available via PM from staff
|
||||
"""
|
||||
import re
|
||||
import random
|
||||
|
||||
from hermes.module import event, command, rule
|
||||
from pprint import pprint, pformat
|
||||
|
||||
key = "quotes"
|
||||
|
||||
def setup(bot):
|
||||
if bot.storage[key] is None:
|
||||
bot.storage[key] = dict()
|
||||
|
||||
|
||||
@event("pubmsg", "privmsg")
|
||||
@command("quote")
|
||||
def quote_admin(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
nick = event.source.nick
|
||||
user = event.source.user
|
||||
host = event.source.host
|
||||
chan = event.target
|
||||
|
||||
if event.type == 'pubmsg':
|
||||
target = event.source.nick if event.type == 'privmsg' else event.target
|
||||
if len(bot.storage[key]) > 0:
|
||||
connection.privmsg(target, random.choice(list(bot.storage[key].values())))
|
||||
return
|
||||
|
||||
if not check_auth(bot, connection, host, nick, False):
|
||||
return
|
||||
|
||||
command = None if len(event.args) == 0 else event.args[0]
|
||||
args = None if len(event.args) < 2 else event.args[1:]
|
||||
|
||||
if command == 'add':
|
||||
quote_add(bot, connection, event, args)
|
||||
elif command == 'del':
|
||||
quote_del(bot, connection, event, args)
|
||||
elif command == 'list' or command is None:
|
||||
quote_list(bot, connection, event, args)
|
||||
|
||||
|
||||
def quote_add(bot, connection, event, args):
|
||||
trigger = None if args is None or len(args) == 0 else args[0]
|
||||
message = None if args is None or len(args) < 2 else args[1:]
|
||||
if trigger is None or message is None:
|
||||
connection.notice(event.source.nick, "Please specify a name and message.")
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
connection.privmsg(event.source.nick, "Quote {0} updated.".format(trigger))
|
||||
else:
|
||||
connection.privmsg(event.source.nick, "Quote {0} added.".format(trigger))
|
||||
|
||||
bot.storage[key][trigger] = " ".join(message)
|
||||
|
||||
|
||||
def quote_del(bot, connection, event, args):
|
||||
trigger = None if args is None or len(args) == 0 else args[0]
|
||||
if trigger is None:
|
||||
connection.notice(event.source.nick, "Please specify a name.")
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
connection.notice(event.source.nick, "Quote {0} deleted.".format(trigger))
|
||||
del bot.storage[key][trigger]
|
||||
else:
|
||||
connection.notice(event.source.nick, "Couldn't find quote {0}.".format(trigger))
|
||||
|
||||
|
||||
def quote_list(bot, connection, event, args):
|
||||
connection.notice(event.source.nick, "Quotes:")
|
||||
for trigger in bot.storage[key].keys():
|
||||
connection.notice(event.source.nick, "{0}: {1}".format(trigger,
|
||||
bot.storage[key][trigger]))
|
||||
|
||||
|
||||
def check_auth(bot, connection, host, nick, prompt):
|
||||
split_host = host.split(".")
|
||||
if len(split_host) != 4:
|
||||
return False
|
||||
|
||||
if host.endswith(bot.config.site.tld):
|
||||
# Make sure that the one issuing the command is authorized to do so
|
||||
user = bot.database.get_user(split_host[0])
|
||||
if user is None:
|
||||
if prompt:
|
||||
connection.notice(nick, "You must be authed through the bot to administer \
|
||||
quotes.")
|
||||
return False
|
||||
|
||||
if user['Level'] >= bot.config.quote.min_level:
|
||||
return True
|
||||
else:
|
||||
if prompt:
|
||||
connection.notice(nick, "You are not authorized to do this command!")
|
||||
return False
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ def show_user(bot, connection, event):
|
||||
chan = event.target.lstrip('#').lower()
|
||||
|
||||
if chan in bot.config.irc.channels:
|
||||
if 'min_level' not in bot.config.irc.channels[chan]:
|
||||
if 'public' in bot.config.irc.channels[chan] and \
|
||||
bot.config.irc.channels[chan].public == True:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -18,13 +18,13 @@ from hermes.module import event, rule, disabled
|
||||
@event("privmsg", "pubmsg")
|
||||
def parse_youtube(bot, connection, event, match):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:param match:
|
||||
:return:
|
||||
:param connection:
|
||||
:param event:
|
||||
:param match:
|
||||
:return:
|
||||
"""
|
||||
if not hasattr(bot.config, "youtube_api"):
|
||||
return
|
||||
@@ -44,4 +44,4 @@ def parse_youtube(bot, connection, event, match):
|
||||
title = str(video['snippet']['title'])
|
||||
views = locale.format("%d", int(video['statistics']['viewCount']), grouping=True)
|
||||
msg = "[ {} | {} views | https://www.youtube.com/watch?v={} ]".format(title, views, video_id)
|
||||
connection.privmsg(target, msg)
|
||||
connection.privmsg(target, msg)
|
||||
|
||||
@@ -47,13 +47,16 @@ def get_git_hash():
|
||||
:return: str hash assuming hermes is in git repo, otherwise None
|
||||
"""
|
||||
git_hash = None
|
||||
if os.path.isdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".git")):
|
||||
current_dir = os.getcwd()
|
||||
git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", )
|
||||
os.chdir(git_dir)
|
||||
out, _ = run_popen("git rev-parse HEAD --short")
|
||||
os.chdir(current_dir)
|
||||
git_hash = str(out, 'utf-8').strip()
|
||||
git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||
try:
|
||||
if os.path.isdir(os.path.join(git_dir, ".git")):
|
||||
current_dir = os.getcwd()
|
||||
os.chdir(git_dir)
|
||||
out, _ = run_popen("git rev-parse HEAD --short")
|
||||
os.chdir(current_dir)
|
||||
git_hash = str(out, 'utf-8').strip()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return git_hash
|
||||
|
||||
|
||||
@@ -101,7 +104,7 @@ class DotDict(dict):
|
||||
__setattr__ = dict.__setitem__
|
||||
__delattr__ = dict.__delitem__
|
||||
__contains__ = dict.__contains__
|
||||
|
||||
|
||||
def __getstate__(self):
|
||||
return self.__dict__
|
||||
def __setstate__(self, d):
|
||||
|
||||
0
run_hermes
Executable file → Normal file
0
run_hermes
Executable file → Normal file
Reference in New Issue
Block a user