mirror of
https://github.com/OPSnet/hermes.git
synced 2026-11-07 06:16:01 -10:00
Initial release of Hermes
This commit is contained in:
commit
463a7fcb9e
28 files changed
+2095
No files matched your search
@@ -0,0 +1,8 @@
|
||||
.idea/
|
||||
.DS_STORE
|
||||
config.yml
|
||||
*.py[cso]
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.swp
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
Hermes
|
||||
======
|
||||
|
||||
An IRC bot built for the next generation of Gazelle sites. It takes ideas and inspiration from
|
||||
[zookeeper](https://github.com/phracker/zookeeper), while providing a much more secure, stable, and extendable
|
||||
platform from which someone can build for their needs.
|
||||
|
||||
The bot is written for Python 3.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
To install, run the setup.py file::
|
||||
|
||||
python3 setup.py install
|
||||
|
||||
|
||||
This will install the bot, and its dependencies, and create a script in /usr/local/bin (or wherever Python is
|
||||
setup to create new bin files) to run the bot through the "hermes" command.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Running hermes::
|
||||
|
||||
bin/hermes
|
||||
|
||||
Hermes primarily runs itself out of the ~/.hermes directory of the user that is running it. Inside, it expects to
|
||||
find a `config.yml` file (explained below), and will log output (if run under the -v or -vv flags) and also
|
||||
load any `.py` files found in `~/.hermes/modules` directory. The bin script will also create a `hermes.pid` file
|
||||
in that directory to prevent multiple instances of the bot from running.
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
The bot is managed through the `config.yml` file (a sample one is provided at `config.yml.sample`). It currently
|
||||
does not make sure that you have all settings set appropriately, so do take care. You just need to copy the example
|
||||
to `~/.hermes/config.yml`, and change it as necessary. It should be relatively straightforward to do.
|
||||
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
import os
|
||||
from hermes import run_hermes
|
||||
from hermes.hermes import HERMES_DIR
|
||||
|
||||
PID = str(os.getpid())
|
||||
os.makedirs(HERMES_DIR, exist_ok=True)
|
||||
PIDFILE = os.path.join(HERMES_DIR, "hermes.pid")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.path.isfile(PIDFILE):
|
||||
print("{} already exists, exiting".format(PIDFILE))
|
||||
raise SystemExit
|
||||
with open(PIDFILE, 'w') as open_pidfile:
|
||||
open_pidfile.write(PID)
|
||||
try:
|
||||
run_hermes()
|
||||
finally:
|
||||
os.unlink(PIDFILE)
|
||||
@@ -0,0 +1,47 @@
|
||||
nick: hermes
|
||||
name: hermes bot
|
||||
site:
|
||||
tld: apollo.rip
|
||||
url: https://apollo.rip
|
||||
irc:
|
||||
host: 127.0.0.1
|
||||
port: 6667
|
||||
channels:
|
||||
apollo:
|
||||
name: apollo
|
||||
devs:
|
||||
name: devs
|
||||
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
|
||||
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
|
||||
channels:
|
||||
- interview
|
||||
- interview2
|
||||
- interview3
|
||||
- interview4
|
||||
- interview5
|
||||
- interview6
|
||||
- interview7
|
||||
- interview8
|
||||
- interview9
|
||||
persist:
|
||||
path: "!HERMES!/persist.dat"
|
||||
admins:
|
||||
- itismadness
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Hermes module
|
||||
"""
|
||||
|
||||
from .hermes import run_hermes, Hermes
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Ephemeral storage of arbitrary data.
|
||||
"""
|
||||
|
||||
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:
|
||||
self.storage = storage
|
||||
else:
|
||||
self.storage = DotDict()
|
||||
|
||||
if expiry is not None:
|
||||
self.expiry = expiry
|
||||
else:
|
||||
self.expiry = timedelta(7)
|
||||
|
||||
def __iter__(self):
|
||||
return self.storage.__iter__()
|
||||
|
||||
def __next__(self):
|
||||
return self.storage.__next__()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.store(key, value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.storage)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.storage
|
||||
|
||||
def store(self, key, value, expiry=None):
|
||||
if not expiry:
|
||||
expiry = self.expiry
|
||||
self.storage[key] = CacheObject(value, datetime.now() + expiry)
|
||||
|
||||
def get(self, key):
|
||||
if key in self.storage:
|
||||
if self.storage[key].expiry > datetime.now():
|
||||
return self.storage[key].value
|
||||
else:
|
||||
del self.storage[key]
|
||||
|
||||
return None
|
||||
|
||||
def clear(self, key=None):
|
||||
if key:
|
||||
if key in self.storage:
|
||||
del self.storage[key]
|
||||
else:
|
||||
self.storage.clear()
|
||||
|
||||
def expire(self):
|
||||
for key in self.storage:
|
||||
if self.storage[key].expiry < datetime.now():
|
||||
del self.storage[key]
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
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
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
import sqlalchemy.dialects.mysql as mysql
|
||||
from sqlalchemy.orm import sessionmaker, relationship
|
||||
from phpserialize import unserialize
|
||||
|
||||
from hermes.utils import dict_to_list
|
||||
|
||||
BASE = declarative_base()
|
||||
|
||||
|
||||
class GazelleDB(object):
|
||||
"""
|
||||
Primary class that is used by hermes to load the DB, and manage it's connection. We then
|
||||
define functions for the interactions that we expect to have with the DB as well such that
|
||||
we are not directly exposing the interface to the end-user so that we're not allowing
|
||||
any arbitrary statement in any of the arbitrary modules.
|
||||
"""
|
||||
def __init__(self, host, db_name, username, password):
|
||||
self._engine = create_engine("mysql+pymysql://{}:{}@{}:33060/{}"
|
||||
.format(username, password, host, db_name), pool_recycle=3600)
|
||||
self._session_maker = sessionmaker(bind=self._engine, autocommit=True)
|
||||
self._session = self._session_maker()
|
||||
|
||||
def get_user(self, username):
|
||||
"""
|
||||
Given a username, get the User that it matches, else return None
|
||||
|
||||
:param username:
|
||||
:return: User that the username belongs to if one exists
|
||||
:rtype: User
|
||||
"""
|
||||
return self._session.query(User).filter_by(username=username).first()
|
||||
|
||||
def get_topic(self, topic_id):
|
||||
"""
|
||||
Given a topic id, get the Topic that it matches, else return None
|
||||
|
||||
:param topic_id:
|
||||
:return: ForumTopics that topic_id belongs to if one exists
|
||||
:rtype: ForumTopics
|
||||
"""
|
||||
return self._session.query(ForumTopics).filter_by(id=int(topic_id)).first()
|
||||
|
||||
def disconnect(self):
|
||||
self._session.close()
|
||||
self._engine.dispose()
|
||||
|
||||
|
||||
class ForumTopics(BASE):
|
||||
__tablename__ = "forums_topics"
|
||||
id = Column('ID', mysql.INTEGER(10, unsigned=True), primary_key=True, autoincrement=True)
|
||||
title = Column('Title', mysql.VARCHAR(150))
|
||||
author_id = Column('AuthorID', mysql.INTEGER(10))
|
||||
is_locked = Column('IsLocked', mysql.ENUM('0', '1'), default='0')
|
||||
is_sticky = Column('IsSticky', mysql.ENUM('0', '1'), default='1')
|
||||
forum_id = Column('ForumID', mysql.INTEGER(3))
|
||||
num_posts = Column('NumPosts', mysql.INTEGER(10))
|
||||
last_post_id = Column('LastPostID', mysql.INTEGER(10))
|
||||
last_post_time = Column('LastPostTime', mysql.DATETIME())
|
||||
last_post_author_id = Column('LastPostAuthorID', mysql.INTEGER(10))
|
||||
sticky_post_id = Column('StickyPostID', mysql.INTEGER(10))
|
||||
ranking = Column('Ranking', mysql.TINYINT(2))
|
||||
created_time = Column('CreatedTime', mysql.DATETIME())
|
||||
|
||||
|
||||
class Permissions(BASE):
|
||||
__tablename__ = "permissions"
|
||||
id = Column('ID', mysql.INTEGER(10, unsigned=True), primary_key=True, autoincrement=True)
|
||||
level = Column('Level', mysql.INTEGER(10, unsigned=True), unique=True)
|
||||
name = Column('Name', mysql.VARCHAR(25))
|
||||
values = Column('Values', mysql.TEXT)
|
||||
display_staff = Column('DisplayStaff', mysql.ENUM('0', '1'), default='0')
|
||||
permitted_forums = Column('PermittedForums', mysql.VARCHAR(150))
|
||||
secondary = Column('Secondary', mysql.TINYINT(4))
|
||||
|
||||
|
||||
class UserInfo(BASE):
|
||||
__tablename__ = "users_info"
|
||||
user_id = Column("UserID", mysql.INTEGER(10, unsigned=True), ForeignKey('users_main.ID'),
|
||||
primary_key=True)
|
||||
style_id = Column("StyleID", mysql.INTEGER(10, unsigned=True))
|
||||
style_url = Column("StyleURL", mysql.VARCHAR(255))
|
||||
info = Column("Info", mysql.TEXT)
|
||||
avatar = Column("Avatar", mysql.VARCHAR(255))
|
||||
admin_comments = Column("AdminComment", mysql.TEXT)
|
||||
site_options = Column("SiteOptions", mysql.TEXT)
|
||||
view_avatars = Column("ViewAvatars", mysql.ENUM('0', '1'))
|
||||
donor = Column('Donor', mysql.ENUM('0', '1'))
|
||||
artist = Column('Artist', mysql.ENUM('0', '1'))
|
||||
download_alt = Column('DownloadAlt', mysql.ENUM('0', '1'))
|
||||
warned = Column('Warned', mysql.DATETIME)
|
||||
support_for = Column('SupportFor', mysql.VARCHAR(255))
|
||||
torrent_grouping = Column('TorrentGrouping', mysql.ENUM('0', '1', '2'))
|
||||
show_tags = Column('ShowTags', mysql.ENUM('0', '1'))
|
||||
notify_on_quote = Column('NotifyOnQuote', mysql.ENUM('0', '1', '2'))
|
||||
auth_key = Column('AuthKey', mysql.VARCHAR(32))
|
||||
reset_key = Column('ResetKey', mysql.VARCHAR(32))
|
||||
reset_expires = Column('ResetExpires', mysql.DATETIME)
|
||||
join_date = Column('JoinDate', mysql.DATETIME)
|
||||
inviter = Column('Inviter', mysql.INTEGER(10))
|
||||
bitcoin_address = Column('BitcoinAddress', mysql.VARCHAR(34))
|
||||
warned_times = Column('WarnedTimes', mysql.INTEGER(2))
|
||||
disable_avatar = Column('DisableAvatar', mysql.ENUM('0', '1'))
|
||||
disable_posting = Column('DisableInvites', mysql.ENUM('0', '1'))
|
||||
disable_forums = Column('DisableForums', mysql.ENUM('0', '1'))
|
||||
disable_irc = Column('DisableIRC', mysql.ENUM('0', '1'))
|
||||
disable_tagging = Column('DisableTagging', mysql.ENUM('0', '1'))
|
||||
disable_upload = Column('DisableUpload', mysql.ENUM('0', '1'))
|
||||
disable_wiki = Column('DisableWiki', mysql.ENUM('0', '1'))
|
||||
disable_pm = Column('DisablePM', mysql.ENUM('0', '1'))
|
||||
ratio_watch_ends = Column('RatioWatchEnds', mysql.DATETIME)
|
||||
ratio_watch_download = Column('RatioWatchDownload', mysql.BIGINT(20, unsigned=True))
|
||||
ratio_watch_times = Column('RatioWatchTimes', mysql.TINYINT(1, unsigned=True))
|
||||
ban_date = Column('BanDate', mysql.DATETIME)
|
||||
ban_reason = Column('BanReason', mysql.ENUM('0', '1', '2', '3', '4'))
|
||||
catchup_time = Column('CatchupTime', mysql.DATETIME)
|
||||
last_read_news = Column('LastReadNews', mysql.INTEGER(10))
|
||||
hide_country_changes = Column('HideCountryChanges', mysql.ENUM('0', '1'))
|
||||
restricted_forums = Column('RestrictedForums', mysql.VARCHAR(150))
|
||||
disable_requests = Column('DisableRequests', mysql.ENUM('0', '1'))
|
||||
permitted_forums = Column('PermittedForums', mysql.VARCHAR(150))
|
||||
unseeded_alerts = Column('UnseededAlerts', mysql.ENUM('0', '1'))
|
||||
last_read_blog = Column('LastReadBlog', mysql.ENUM('0', '1'))
|
||||
info_title = Column('InfoTitle', mysql.VARCHAR(255))
|
||||
|
||||
|
||||
class User(BASE):
|
||||
__tablename__ = "users_main"
|
||||
id = Column("ID", mysql.INTEGER(10, unsigned=True), primary_key=True)
|
||||
username = Column('Username', mysql.VARCHAR(20))
|
||||
email = Column('Email', mysql.VARCHAR(255))
|
||||
pass_hash = Column('PassHash', mysql.VARCHAR(60))
|
||||
secret = Column('Secret', mysql.VARCHAR(32))
|
||||
irc_key = Column('IRCKey', mysql.CHAR(32))
|
||||
last_login = Column('LastLogin', mysql.DATETIME)
|
||||
last_access = Column('LastAccess', mysql.DATETIME)
|
||||
ip = Column('IP', mysql.VARCHAR(15))
|
||||
class_id = Column('Class', mysql.TINYINT(2))
|
||||
uploaded = Column('Uploaded', mysql.BIGINT(20, unsigned=True))
|
||||
downloaded = Column('Downloaded', mysql.BIGINT(20, unsigned=True))
|
||||
title = Column('Title', mysql.TEXT)
|
||||
enabled = Column('Enabled', mysql.ENUM('0', '1', '2'))
|
||||
paranoia = Column('Paranoia', mysql.TEXT)
|
||||
visible = Column('Visible', mysql.ENUM('1', '0'))
|
||||
invites = Column('Invites', mysql.INTEGER(10, unsigned=True))
|
||||
permission_id = Column('PermissionID', mysql.INTEGER(10, unsigned=True),
|
||||
ForeignKey('permissions.ID'))
|
||||
custom_permissions = Column('CustomPermissions', mysql.TEXT)
|
||||
can_leech = Column('can_leech', mysql.TINYINT(4))
|
||||
torrent_pass = Column('torrent_pass', mysql.CHAR(32))
|
||||
required_ratio = Column('RequiredRatio', mysql.DOUBLE(10, 8))
|
||||
required_ratio_work = Column('RequiredRatioWork', mysql.DOUBLE(10, 8))
|
||||
ipcc = Column('ipcc', mysql.VARCHAR(2))
|
||||
fl_tokens = Column('FLTokens', mysql.INTEGER(10))
|
||||
|
||||
permission_class = relationship("Permissions")
|
||||
|
||||
info = relationship("UsersInfo")
|
||||
|
||||
def get_paranoia(self):
|
||||
return dict_to_list(unserialize(bytes(str(self.paranoia), 'utf-8')))
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Module that contains all of the different types of events that hermes can act upon. Useful just
|
||||
for ensuring consistency in reference to the events across the hermes codebase. The events should
|
||||
always be lowercased with an uppercase const reference.
|
||||
"""
|
||||
|
||||
|
||||
class Events(object):
|
||||
PRIVMSG = "privmsg"
|
||||
PUBMSG = "pubmsg"
|
||||
@@ -0,0 +1,454 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
import argparse
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from irc.bot import SingleServerIRCBot
|
||||
from irc.connection import Factory
|
||||
|
||||
from .database import GazelleDB
|
||||
from .loader import load_modules
|
||||
from .utils import get_git_hash, check_pid, load_config, DotDict
|
||||
from .cache import Cache
|
||||
from .persist import PersistentStorage
|
||||
|
||||
locale.setlocale(locale.LC_ALL, 'en_US.utf8')
|
||||
__version__ = "0.2.0"
|
||||
|
||||
LOGGER = logging.getLogger("hermes")
|
||||
HERMES_DIR = os.path.join(os.path.expanduser("~"), ".hermes")
|
||||
|
||||
|
||||
def set_verbosity(verbose=0, level=logging.INFO):
|
||||
log_format = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")
|
||||
if verbose >= 1:
|
||||
file_logger = logging.FileHandler(os.path.join(HERMES_DIR, "hermes.log"))
|
||||
file_logger.setFormatter(log_format)
|
||||
file_logger.setLevel(level)
|
||||
LOGGER.addHandler(file_logger)
|
||||
if verbose == 2:
|
||||
console_logger = logging.StreamHandler(sys.stdout)
|
||||
console_logger.setFormatter(log_format)
|
||||
console_logger.setLevel(level)
|
||||
LOGGER.addHandler(console_logger)
|
||||
else:
|
||||
LOGGER.addHandler(logging.NullHandler())
|
||||
LOGGER.setLevel(level)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
||||
class Hermes(SingleServerIRCBot):
|
||||
def __init__(self):
|
||||
self.logger = LOGGER
|
||||
self.dir = HERMES_DIR
|
||||
self.logger.info("-> Loading Hermes ({})".format(get_version_string()))
|
||||
if not os.path.isdir(HERMES_DIR):
|
||||
os.makedirs(HERMES_DIR, exist_ok=True)
|
||||
self.config = load_config(os.path.join(HERMES_DIR, "config.yml"))
|
||||
self.logger.info("-> Loaded Config")
|
||||
self.nick = self.config.nick
|
||||
self.name = self.config.name if 'name' in self.config else self.nick
|
||||
|
||||
self.logger.info("-> Loading Modules")
|
||||
self.modules = load_modules()
|
||||
self.logger.info("-> Modules Loaded")
|
||||
|
||||
if 'persist' in self.config and 'path' in self.config.persist:
|
||||
persist_path = self.config.persist.path.replace('!HERMES!', self.dir)
|
||||
else:
|
||||
persist_path = os.path.join(self.dir, 'persist.dat')
|
||||
|
||||
self.storage = PersistentStorage(persist_path)
|
||||
|
||||
self.logger.info("-> Loaded Storage ({0} keys)".format(len(self.storage)))
|
||||
if 'cache' not in self.storage:
|
||||
self.storage['cache'] = DotDict()
|
||||
|
||||
self.cache = Cache(self.storage['cache'])
|
||||
|
||||
self.logger.info("-> Loaded Cache ({0} keys)".format(len(self.cache)))
|
||||
|
||||
self.listener = None
|
||||
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.logger.info("-> Loaded DB")
|
||||
|
||||
for name, mod in self.modules.items():
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if hasattr(mod, 'setup'):
|
||||
mod.setup(self)
|
||||
self.logger.info("Loaded module: {}".format(name))
|
||||
except:
|
||||
self.logger.exception("Error Module: {}".format(name))
|
||||
|
||||
if 'ssl' in self.config.irc and self.config.irc.ssl is True:
|
||||
factory = Factory(wrapper=ssl.wrap_socket)
|
||||
else:
|
||||
factory = Factory()
|
||||
|
||||
super().__init__([(self.config.irc.host, self.config.irc.port)],
|
||||
'{}{}'.format(self.nick, random.randint(1, 1000)), self.name,
|
||||
connect_factory=factory)
|
||||
for attr in ("on_pubmsg", "on_privmsg"):
|
||||
setattr(self, attr, self._dispatch)
|
||||
self.logger.info("-> Loaded IRC")
|
||||
|
||||
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).
|
||||
|
||||
:raises: SystemError
|
||||
"""
|
||||
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.
|
||||
|
||||
:raises: SystemError
|
||||
"""
|
||||
raise SystemError("*** ERROR: Illegal characters in BotNick ***")
|
||||
|
||||
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
|
||||
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
self.logger.info("-> Connected to {} with nick {}".format(self.config.irc.host,
|
||||
self.nick))
|
||||
if hasattr(self.config.irc, "oper"):
|
||||
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.listener.set_connection(connection)
|
||||
self.listener.start()
|
||||
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))
|
||||
|
||||
def on_disconnect(self, connection, event):
|
||||
self.logger.info("-> Disconnected from IRC")
|
||||
|
||||
def _execute_function(self, func, connection, event):
|
||||
cmd = event.cmd
|
||||
if hasattr(func, "commands"):
|
||||
for command in func.commands:
|
||||
if cmd == "." + command or cmd == "!" + command or \
|
||||
(event.type == "privmsg" and cmd == command):
|
||||
func(self, connection, event)
|
||||
if hasattr(func, "rules"):
|
||||
for rule in func.rules:
|
||||
match = re.search(rule, event.msg)
|
||||
if match:
|
||||
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
|
||||
|
||||
def _dispatch(self, connection, event):
|
||||
"""
|
||||
:param connection:
|
||||
: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 -
|
||||
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)
|
||||
tags (empty list)
|
||||
"""
|
||||
event.msg = event.arguments[0]
|
||||
args = event.arguments[0].split()
|
||||
if len(args) == 0:
|
||||
return
|
||||
event.cmd = args[0].lower()
|
||||
event.args = args[1:] if len(args) > 1 else []
|
||||
for name, mod in self.modules.items():
|
||||
for func in mod.__callables__:
|
||||
if func.disabled is True:
|
||||
continue
|
||||
elif func.admin_only is True and not self.check_admin(event):
|
||||
continue
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if event.type in func.events:
|
||||
self._execute_function(func, connection, event)
|
||||
except:
|
||||
if event.type == "privmsg":
|
||||
msg = "I'm sorry, {}.{} threw an exception. Please tell an " \
|
||||
"administrator and try again later.".format(name, func.__name__)
|
||||
connection.privmsg(event.source.nick, msg)
|
||||
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:
|
||||
self.database.disconnect()
|
||||
if self.listener is not None:
|
||||
self.listener.stop()
|
||||
super(Hermes, self).disconnect(msg)
|
||||
|
||||
def restart(self):
|
||||
self.disconnect()
|
||||
raise RestartException
|
||||
|
||||
|
||||
class RestartException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BotCheck(threading.Thread):
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.alive = True
|
||||
self.bot = bot
|
||||
|
||||
def run(self):
|
||||
time.sleep(120)
|
||||
while self.alive:
|
||||
if len(self.bot.channels) == 0:
|
||||
self.bot.logger.info('-> Bot not connected to channels, restarting')
|
||||
self.bot.restart()
|
||||
time.sleep(5)
|
||||
|
||||
def stop(self):
|
||||
self.alive = False
|
||||
self.join()
|
||||
|
||||
class SaveData(threading.Thread):
|
||||
def __init__(self, bot):
|
||||
super().__init__()
|
||||
self.alive = True
|
||||
self.bot = bot
|
||||
|
||||
def run(self):
|
||||
time.sleep(120)
|
||||
while self.alive:
|
||||
self.bot.storage.save()
|
||||
time.sleep(600)
|
||||
|
||||
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.
|
||||
"""
|
||||
def __init__(self, host, port):
|
||||
self.logger = LOGGER
|
||||
self.running = True
|
||||
self.restart = False
|
||||
self.connection = None
|
||||
self.host = host
|
||||
self.port = port
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def set_connection(self, connection):
|
||||
self.connection = connection
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
client_socket.connect((self.host, self.port))
|
||||
# client_socket.send("QUITTING")
|
||||
client_socket.close()
|
||||
|
||||
def run(self):
|
||||
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))
|
||||
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.bind((self.host, self.port))
|
||||
server_socket.listen(5)
|
||||
client_socket, address = server_socket.accept()
|
||||
data = client_socket.recv(512).decode('utf-8').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] == "#":
|
||||
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]))
|
||||
server_socket.close()
|
||||
|
||||
|
||||
def get_version_string():
|
||||
version_string = __version__
|
||||
git_hash = get_git_hash()
|
||||
if git_hash is not None:
|
||||
version_string += "-{}".format(git_hash)
|
||||
return 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.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def run_hermes():
|
||||
args = _parse_args()
|
||||
levels = {'debug': logging.DEBUG, 'info': logging.INFO, 'warn': logging.WARN,
|
||||
'error': logging.ERROR}
|
||||
log_level = levels[args.log_level]
|
||||
set_verbosity(args.verbose, log_level)
|
||||
run_eternal = args.no_eternal is not True
|
||||
|
||||
if os.getuid() == 0 or os.geteuid() == 0:
|
||||
raise SystemExit('Error: Do not run Hermes with root privileges.')
|
||||
|
||||
os.makedirs(HERMES_DIR, exist_ok=True)
|
||||
|
||||
pidfile = os.path.join(HERMES_DIR, "hermes.pid")
|
||||
if os.path.isfile(pidfile):
|
||||
with open(pidfile, 'r') as open_pidfile:
|
||||
try:
|
||||
old_pid = int(open_pidfile.read().strip())
|
||||
except ValueError:
|
||||
old_pid = None
|
||||
if old_pid is not None and check_pid(old_pid):
|
||||
if args.stop:
|
||||
print("Stopping instance of Hermes ({})".format(old_pid))
|
||||
os.kill(old_pid, signal.SIGTERM)
|
||||
elif args.kill:
|
||||
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()
|
||||
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
|
||||
# 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:
|
||||
raise SystemExit
|
||||
|
||||
with open(pidfile, 'w') as open_pidfile:
|
||||
open_pidfile.write(str(os.getpid()))
|
||||
|
||||
last_run = None
|
||||
|
||||
try:
|
||||
hermes = Hermes()
|
||||
save_thread = SaveData(hermes)
|
||||
save_thread.start()
|
||||
#thread = BotCheck(hermes)
|
||||
#thread.start()
|
||||
|
||||
def signal_handler(sig, _):
|
||||
if sig is signal.SIGTERM:
|
||||
hermes.disconnect()
|
||||
else:
|
||||
hermes.die()
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
#signal.signal(signal.SIGKILL, signal_handler)
|
||||
while run_eternal:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
last_run = time.time()
|
||||
hermes.start()
|
||||
except (KeyboardInterrupt, SystemError, SystemExit) as e:
|
||||
LOGGER.info("-> {}".format(e))
|
||||
hermes.disconnect("Leaving...")
|
||||
LOGGER.info("Quitting bot")
|
||||
#thread.stop()
|
||||
raise SystemExit
|
||||
except RestartException:
|
||||
#thread.stop()
|
||||
time.sleep(5)
|
||||
hermes = Hermes()
|
||||
#thread = BotCheck(hermes)
|
||||
#thread.start()
|
||||
hermes.start()
|
||||
except:
|
||||
if last_run > time.time() - 5:
|
||||
hermes.disconnect("Crashed, going offline.")
|
||||
run_eternal = False
|
||||
else:
|
||||
hermes.disconnect("Crashed, going to reboot...")
|
||||
time.sleep(2)
|
||||
LOGGER.exception("Crash")
|
||||
finally:
|
||||
os.unlink(pidfile)
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Module for that allows for dynamic loading/reloading of modules for hermes to use such that we
|
||||
don't have to necessarily build all of our functionality into the core of hermes, but rather
|
||||
in separate files each with its own purpose that we've then registered to be run given a
|
||||
certain command (like getting a privmsg) or matches some regex rule.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def load_modules():
|
||||
"""
|
||||
Loads all the modules from the the modules/ directory within hermes and then checks if
|
||||
the ~/hermes/modules folder exists and then loads any modules there as well (if it exists).
|
||||
Note, we want all modules to have unique names otherwise any in ~/hermes/modules will overwrite
|
||||
any in the modules/ directory.
|
||||
|
||||
:return: a dictionary containing all loaded modules where they key is module name (filename)
|
||||
and the value is the loaded module definition
|
||||
"""
|
||||
modules = {}
|
||||
modules_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules")
|
||||
modules.update(_get_all_modules(modules_dir))
|
||||
local_dir = os.path.join(os.path.expanduser("~/hermes"), "modules")
|
||||
if os.path.isdir(local_dir):
|
||||
modules.update(_get_all_modules(local_dir))
|
||||
return modules
|
||||
|
||||
|
||||
def _get_all_modules(directory):
|
||||
modules = {}
|
||||
for path in os.listdir(directory):
|
||||
module_path = os.path.join(directory, path)
|
||||
if os.path.isfile(module_path) and module_path.endswith(".py"):
|
||||
load_module(modules, module_path)
|
||||
return modules
|
||||
|
||||
|
||||
def load_module(modules, module_path):
|
||||
name = os.path.basename(module_path)[:-3]
|
||||
spec = importlib.util.spec_from_file_location(name, module_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
mod.__callables__ = parse_module(mod)
|
||||
mod.__mod_path__ = module_path
|
||||
modules[name] = mod
|
||||
sys.modules[name] = mod
|
||||
|
||||
|
||||
def _parse_callable(obj):
|
||||
obj.admin_only = getattr(obj, "admin_only", False) is True
|
||||
obj.disabled = getattr(obj, 'disabled', False) is True
|
||||
if not hasattr(obj, 'events'):
|
||||
obj.events = ['pubmsg']
|
||||
else:
|
||||
if isinstance(obj.events, str):
|
||||
obj.events = [obj.events]
|
||||
obj.events = [event.lower() for event in obj.events]
|
||||
if hasattr(obj, 'commands'):
|
||||
if isinstance(obj.commands, str):
|
||||
obj.commands = [obj.commands]
|
||||
obj.commands = [command.lower() for command in obj.commands]
|
||||
if hasattr(obj, 'rules'):
|
||||
if isinstance(obj.rules, str):
|
||||
obj.rules = [[obj.rules, 0]]
|
||||
assert(isinstance(obj.rules, list))
|
||||
for i in range(len(obj.rules)):
|
||||
obj.rules[i] = re.compile(*obj.rules[i])
|
||||
|
||||
obj.help = getattr(obj, "help", None)
|
||||
obj.examples = getattr(obj, "examples", [])
|
||||
return obj
|
||||
|
||||
|
||||
def parse_module(mod):
|
||||
callables = []
|
||||
for name, func in inspect.getmembers(mod, inspect.isfunction):
|
||||
if any(hasattr(func, attr) for attr in ('rules', 'commands')):
|
||||
callables.append(_parse_callable(func))
|
||||
return callables
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Module defines various decorators we can assign to given module functions so that we have a nice
|
||||
pluggable interface for expanding the bots abilities and functions in a way that doesn't actually
|
||||
mean we have to hack apart the core bot code.
|
||||
"""
|
||||
|
||||
|
||||
def pubmsg():
|
||||
"""
|
||||
Decorator
|
||||
:return:
|
||||
"""
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "events"):
|
||||
func.events = []
|
||||
func.events.append("pubmsg")
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def privmsg():
|
||||
"""
|
||||
Defines a function that should only be run when we've received a private message
|
||||
:return:
|
||||
"""
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "events"):
|
||||
func.events = []
|
||||
func.events.append("privmsg")
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def command(*commands):
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "commands"):
|
||||
func.commands = []
|
||||
func.commands.extend(commands)
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def event(*events):
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "events"):
|
||||
func.events = []
|
||||
func.events.extend(events)
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def rule(regex, flags=0):
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "rules"):
|
||||
func.rules = []
|
||||
func.rules.append([regex, flags])
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def help_message(message):
|
||||
def add_attribute(func):
|
||||
func.help = message
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def example(*examples):
|
||||
def add_attribute(func):
|
||||
if not hasattr(func, "examples"):
|
||||
func.examples = []
|
||||
func.examples.extend(examples)
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def admin_only():
|
||||
def add_attribute(func):
|
||||
func.admin_only = True
|
||||
return func
|
||||
return add_attribute
|
||||
|
||||
|
||||
def disabled():
|
||||
def add_attribute(func):
|
||||
func.disabled = True
|
||||
return func
|
||||
return add_attribute
|
||||
Whitespace-only changes.
@@ -0,0 +1,30 @@
|
||||
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)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Module for governing certain admin commands for the bot such as restarting it, updating it (via
|
||||
git), killing the bot, and printing out its version.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from hermes.hermes import get_version_string
|
||||
from hermes.module import admin_only, privmsg, command
|
||||
from hermes.utils import run_popen, file_tail
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("restart")
|
||||
def restart_bot(bot, connection, event):
|
||||
connection.privmsg(event.source.nick, "Saving persistent data")
|
||||
bot.logger.info("-> Saving data")
|
||||
bot.storage.save()
|
||||
connection.privmsg(event.source.nick, "Restarting the bot now")
|
||||
bot.logger.info("-> Restarting bot")
|
||||
pidfile = os.path.join(bot.dir, "hermes.pid")
|
||||
if os.path.isfile(pidfile):
|
||||
os.unlink(pidfile)
|
||||
os.execl(sys.executable, *([sys.executable] + sys.argv))
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command('restart_socket')
|
||||
def restart_socket(bot, connection, event):
|
||||
if bot.listener is not None:
|
||||
connection.privmsg(event.source.nick, 'Restarting the socket')
|
||||
bot.listener.restart = True
|
||||
else:
|
||||
connection.privmsg(event.source.nick, 'Not using socket')
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("update")
|
||||
def update_bot(bot, connection, event):
|
||||
|
||||
git_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
|
||||
|
||||
if os.path.isdir(os.path.join(git_dir, ".git")):
|
||||
current_dir = os.getcwd()
|
||||
os.chdir(git_dir)
|
||||
get_version(bot, connection, event)
|
||||
out, err = run_popen("git pull")
|
||||
connection.privmsg(event.source.nick, "Running git pull...")
|
||||
bot.logger.info("-> Updating bot, running git pull")
|
||||
bot.logger.info(str(out, "utf-8"))
|
||||
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)
|
||||
else:
|
||||
connection.privmsg(event.source.nick, "Can only update bot if run from git directory.")
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("kill")
|
||||
def kill_bot(bot, *_):
|
||||
bot.logger.info("-> Killing bot")
|
||||
raise SystemExit
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("version")
|
||||
def get_version(_, connection, event):
|
||||
connection.privmsg(event.source.nick, "Running version: {}".format(get_version_string()))
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command('view_log')
|
||||
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)
|
||||
except Exception as e:
|
||||
connection.privmsg(event.source.nick, e)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Module to administrate the cache
|
||||
"""
|
||||
|
||||
from hermes.module import event, command, admin_only
|
||||
|
||||
@event('privmsg')
|
||||
@command('cache')
|
||||
@admin_only()
|
||||
def cache_control(bot, connection, event):
|
||||
if len(event.args) < 1:
|
||||
pass
|
||||
nick = event.source.nick
|
||||
command = event.args[0]
|
||||
if command == 'clear':
|
||||
if len(event.args) == 1:
|
||||
bot.cache.clear()
|
||||
connection.privmsg(nick, "Cache cleared")
|
||||
else:
|
||||
for key in event.args[1:]:
|
||||
bot.cache.clear(key)
|
||||
connection.privmsg(nick, "Cleared cache keys {0}".format(", ".join(event.args[1:])))
|
||||
elif command == 'count':
|
||||
connection.privmsg(nick, "Cache keys: {0}".format(str(len(bot.cache))))
|
||||
elif command == 'expire':
|
||||
bot.cache.expire()
|
||||
connection.privmsg(nick, "Expired cache keys purged")
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Module that allows good-standing members (don't have IRC privileges revoked) to enter the
|
||||
official IRC channels (which should all be +i and not regularly joinable), as well as to have
|
||||
their ident and host be changed such that they'll be <user_id>@<user_name>.<group_name>.<site_tld>
|
||||
which allows for better anonymization than default IRC.
|
||||
|
||||
Note, because the bot is using SAJOIN to add users to the channels that they request to enter,
|
||||
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
|
||||
|
||||
|
||||
def validate_irckey(user, irckey):
|
||||
if user == None:
|
||||
return False, "No user found with that name."
|
||||
else:
|
||||
if user.Enabled != '1' or user.DisableIRC == '1':
|
||||
return False, "Your IRC privileges have been disabled. Join #disabled."
|
||||
elif user.IRCKey == irckey:
|
||||
return True, ""
|
||||
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")
|
||||
def enter(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
|
||||
sent_nick = event.source.nick
|
||||
if len(event.args) < 3:
|
||||
connection.privmsg(sent_nick, "Not enough arguments for enter command")
|
||||
connection.privmsg(sent_nick, "enter <channels> <username> <password>")
|
||||
return
|
||||
username = event.args[-2]
|
||||
password = event.args[-1]
|
||||
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)))
|
||||
|
||||
# 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:
|
||||
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))
|
||||
joined = []
|
||||
not_real = []
|
||||
not_joined = []
|
||||
for channel in channels:
|
||||
channel = channel.lstrip('#')
|
||||
real_channel = None
|
||||
for bot_channel in bot.config.irc.channels:
|
||||
if channel.lower() == bot_channel.lower():
|
||||
real_channel = bot_channel
|
||||
break
|
||||
|
||||
# Is this a channel the bot is actively watching and cares about?
|
||||
if real_channel is not None:
|
||||
join_channel = True
|
||||
# Does the channel have a minimum level set and does the user's class exceed it?
|
||||
if 'min_level' in bot.config.irc.channels[real_channel]:
|
||||
if bot.config.irc.channels[real_channel].min_level > int(user.Level):
|
||||
join_channel = False
|
||||
# Does the channel allow certain classes in the channel and does the user
|
||||
# have that class as either the primary class or one of his secondary classes?
|
||||
if not join_channel and 'classes' in bot.config.irc.channels[real_channel]:
|
||||
# get intersection of the user's primary class, secondary classes, and classes
|
||||
# that are allowed for the channel
|
||||
intersect = set(user.SecondaryClasses + [user.Class]) & \
|
||||
set(bot.config.irc.channels[real_channel].classes)
|
||||
if len(intersect) > 0:
|
||||
join_channel = True
|
||||
if join_channel:
|
||||
joined.append(real_channel)
|
||||
connection.send_raw("SAJOIN {} #{}".format(sent_nick, real_channel))
|
||||
else:
|
||||
not_joined.append(channel)
|
||||
else:
|
||||
not_real.append(channel)
|
||||
|
||||
if len(not_real) > 0:
|
||||
connection.privmsg(sent_nick,
|
||||
"I'm sorry, the channels #{} do not "
|
||||
"exist".format(", #".join(not_real)))
|
||||
|
||||
if len(not_joined) > 0:
|
||||
connection.privmsg(sent_nick,
|
||||
"I'm sorry, you lack the right permissions to "
|
||||
"join #{}".format(", #".join(not_joined)))
|
||||
|
||||
if len(joined) > 0:
|
||||
connection.privmsg(sent_nick, "Welcome to #{}".format(", #".join(joined)))
|
||||
bot.logger.debug("-> {} entered #{}".format(sent_nick, ", #".join(joined)))
|
||||
else:
|
||||
connection.privmsg(sent_nick, error)
|
||||
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Module to provide FLS with canned responses.
|
||||
Administration is available from the designated FLS channel or via PM from FLS / staff
|
||||
"""
|
||||
import re
|
||||
|
||||
from hermes.module import event, command, rule
|
||||
from pprint import pprint, pformat
|
||||
|
||||
key = "canned_responses"
|
||||
|
||||
def setup(bot):
|
||||
if bot.storage[key] is None:
|
||||
bot.storage[key] = dict()
|
||||
|
||||
|
||||
@event("pubmsg", "privmsg")
|
||||
@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
|
||||
if trigger in bot.storage[key]:
|
||||
connection.privmsg(target, bot.storage[key][trigger])
|
||||
|
||||
|
||||
@event("pubmsg", "privmsg")
|
||||
@command("can")
|
||||
def can_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 == 'privmsg':
|
||||
if not check_auth(bot, connection, host, nick, False):
|
||||
return
|
||||
else:
|
||||
if not chan.strip('#').lower() == bot.config.fls.channel.lower():
|
||||
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':
|
||||
can_add(bot, connection, event, args)
|
||||
elif command == 'del':
|
||||
can_del(bot, connection, event, args)
|
||||
elif command == 'list' or command is None:
|
||||
can_list(bot, connection, event, args)
|
||||
|
||||
|
||||
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.")
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
connection.privmsg(event.source.nick, "Trigger {0} updated.".format(
|
||||
trigger))
|
||||
else:
|
||||
connection.privmsg(event.source.nick, "Trigger {0} added.".format(
|
||||
trigger))
|
||||
|
||||
bot.storage[key][trigger] = " ".join(message)
|
||||
|
||||
|
||||
def can_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 trigger.")
|
||||
return
|
||||
|
||||
if trigger in bot.storage[key]:
|
||||
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))
|
||||
|
||||
|
||||
def can_list(bot, connection, event, args):
|
||||
connection.notice(event.source.nick, "Triggers:")
|
||||
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 canned responses.")
|
||||
return False
|
||||
|
||||
if bot.config.fls.class_id in user['SecondaryClasses'] or \
|
||||
user['Level'] >= bot.config.fls.min_level:
|
||||
return True
|
||||
else:
|
||||
if prompt:
|
||||
connection.notice(nick, "You are not authorized to do this command!")
|
||||
return False
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
This module creates a help screen for the user, displaying all of the commands that the bot can
|
||||
understand based on the registered modules. It will by default just show the command and whether
|
||||
it can be issued in pubmsg, privmsg or both. However, if the @help_message and/or @example
|
||||
annotations are filled out, then it will show those as well as the command.
|
||||
"""
|
||||
|
||||
from hermes.module import event, command, help_message
|
||||
|
||||
|
||||
@event("privmsg", "pubmsg")
|
||||
@command("help")
|
||||
@help_message("Display this help message")
|
||||
def show_help(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
target = event.source.nick
|
||||
connection.privmsg(target, "Hello, I'm the friendly IRC bot.")
|
||||
connection.privmsg(target, "The following commands can be private messaged to me:")
|
||||
connection.privmsg(target, "------------------")
|
||||
_show_inner_help("privmsg", bot, connection, event)
|
||||
connection.privmsg(target, "The following commands can be used in public channels:")
|
||||
connection.privmsg(target, "------------------")
|
||||
_show_inner_help("pubmsg", bot, connection, event)
|
||||
|
||||
|
||||
def _show_inner_help(type, bot, connection, event):
|
||||
target = event.source.nick
|
||||
for name, mod in bot.modules.items():
|
||||
for func in mod.__callables__:
|
||||
if type in func.events:
|
||||
if func.admin_only is True and not bot.check_admin(event):
|
||||
continue
|
||||
if hasattr(func, "rules"):
|
||||
continue
|
||||
if hasattr(func, "commands"):
|
||||
connection.privmsg(target,
|
||||
"Command(s): {}".format(", ".join(func.commands)))
|
||||
if func.help is not None:
|
||||
connection.privmsg(target, "Description: {}".format(func.help))
|
||||
if len(func.examples) > 0:
|
||||
for example in func.examples:
|
||||
connection.privmsg(target, "Example: {}".format(example))
|
||||
connection.privmsg(target, "------------------")
|
||||
@@ -0,0 +1,269 @@
|
||||
"""
|
||||
Module to handle interview queues
|
||||
"""
|
||||
|
||||
from hermes.module import event, command, disabled, admin_only
|
||||
from time import time
|
||||
import re
|
||||
|
||||
key = "interview_queue"
|
||||
|
||||
def setup(bot):
|
||||
if bot.storage[key] is None:
|
||||
bot.storage[key] = list()
|
||||
|
||||
|
||||
def convert_time(diff):
|
||||
seconds = diff % 60
|
||||
diff = (diff - seconds) / 60
|
||||
minutes = diff % 60
|
||||
diff = (diff - minutes) / 60
|
||||
hours = diff % 24
|
||||
days = (diff - hours) / 24
|
||||
return days, hours, minutes, seconds
|
||||
|
||||
|
||||
class UserClass:
|
||||
def __init__(self, nick, host, user, speed_test):
|
||||
self.nick = nick
|
||||
self.host = host
|
||||
self.user = user
|
||||
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 " \
|
||||
"{} seconds".format(str(round(days)), str(round(hours)), str(round(minutes)),
|
||||
str(round(seconds)))
|
||||
|
||||
def get_full_name(self):
|
||||
return "{}!{}@{}".format(self.nick, self.user, self.host)
|
||||
|
||||
|
||||
def is_in_queue(bot, user, host):
|
||||
is_already_in_queue = False
|
||||
position = 0
|
||||
for i in range(len(bot.storage[key])):
|
||||
if user == bot.storage[key][i].user and host == bot.storage[key][i].host:
|
||||
is_already_in_queue = True
|
||||
position = i
|
||||
return is_already_in_queue, position
|
||||
|
||||
|
||||
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 start an interview.")
|
||||
return False
|
||||
|
||||
if bot.config.interview.class_id in user['SecondaryClasses'] or \
|
||||
user['Level'] >= bot.config.interview.min_level:
|
||||
return True
|
||||
else:
|
||||
if prompt:
|
||||
connection.notice(nick, "You are not authorized to do this command!")
|
||||
return False
|
||||
|
||||
|
||||
def is_in_channel(user, channel):
|
||||
for chan_user in channel.users():
|
||||
if user.nick.lower() == chan_user.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def next_user(bot, connection):
|
||||
while len(bot.storage[key]) > 0:
|
||||
user = bot.storage[key].pop(0)
|
||||
if is_in_channel(user, bot.channels['#recruitment']):
|
||||
return user
|
||||
return None
|
||||
|
||||
|
||||
@event("privmsg")
|
||||
@command("queue")
|
||||
def queue(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
|
||||
|
||||
if check_auth(bot, connection, host, nick, False):
|
||||
if len(bot.storage[key]) == 0:
|
||||
connection.notice(nick, "The queue is empty")
|
||||
else:
|
||||
connection.notice(nick, "The queue currently has {} people in it:".format(len(bot.storage[key])))
|
||||
for x in range(len(bot.storage[key])):
|
||||
person = bot.storage[key][x]
|
||||
connection.notice(nick, "{}) {} - {}".format(x+1, person.nick, person.get_waited_str()))
|
||||
else:
|
||||
if len(event.args) == 0:
|
||||
connection.notice(nick, "The queue command requires a speedtest URL.")
|
||||
return
|
||||
|
||||
speed_test = event.args[0]
|
||||
|
||||
if 'speedtest_urls' in bot.config.interview and isinstance(bot.config.interview.speedtest_urls,
|
||||
list):
|
||||
match = next(re.match(regex, speed_test) for regex in bot.config.interview.speedtest_urls)
|
||||
if match:
|
||||
if len(match.groups()) > 0:
|
||||
speed_test = "https://www.speedtest.net/result/{0}.png".format(match.group(1))
|
||||
else:
|
||||
connection.notice(nick, "Speedtest URL must be directly to the image, refer to "
|
||||
"Starting the Interview at {} if you are "
|
||||
"unsure.".format(bot.config.interview.site))
|
||||
return
|
||||
|
||||
is_already_in_queue, position = is_in_queue(bot, user, host)
|
||||
if is_already_in_queue:
|
||||
connection.notice(nick, "You are already in the queue at position "
|
||||
"{}.".format(str(position + 1)))
|
||||
else:
|
||||
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]))))
|
||||
bot.logger.debug("Successfully queued {} at {}.".format(nick, str(len(bot.storage[key]))))
|
||||
|
||||
|
||||
@event("privmsg")
|
||||
@command("info")
|
||||
def info(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
|
||||
|
||||
is_already_in_queue, position = is_in_queue(bot, user, host)
|
||||
if is_already_in_queue:
|
||||
connection.notice(nick, "You are in the queue at position {}.".format(str(position + 1)))
|
||||
connection.notice(nick, "You have waited for {}.".format(bot.storage[key][position].get_waited_str()))
|
||||
else:
|
||||
connection.notice(nick, "You are not in the queue. To enter it type "
|
||||
"!queue <speedtest URL>.")
|
||||
|
||||
|
||||
@event("privmsg", "pubmsg")
|
||||
@command("next")
|
||||
def next_interview(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
nick = event.source.nick
|
||||
host = event.source.host
|
||||
|
||||
if len(event.args) == 0:
|
||||
connection.notice(nick, "The next command requires a channel name.")
|
||||
return
|
||||
|
||||
chan = event.args[0].lstrip('#')
|
||||
|
||||
if check_auth(bot, connection, host, nick, True):
|
||||
if chan.lower() not in bot.config.interview.channels:
|
||||
connection.notice(nick, "Must use an official interview channel: #{}".format(
|
||||
", #".join(bot.config.interview.channels)))
|
||||
return
|
||||
|
||||
user = next_user(bot, connection)
|
||||
|
||||
if not user:
|
||||
connection.notice(nick, "Cannot perform command, queue is empty.")
|
||||
else:
|
||||
connection.notice(nick, "Next candidate is {}. Speedtest URL is "
|
||||
"{}.".format(user.get_full_name(), user.speed_test))
|
||||
connection.send_raw("SAJOIN {} #{}".format(user.nick, chan))
|
||||
connection.privmsg(user.nick, "You have been have been invited to take your "
|
||||
"interview in #{}.".format(chan))
|
||||
|
||||
|
||||
@event("privmsg")
|
||||
@command("queue_length")
|
||||
def queue_length(bot, connection, event):
|
||||
connection.notice(event.source.nick, "The queue currently has {} people "
|
||||
"in it.".format(len(bot.storage[key])))
|
||||
|
||||
|
||||
@event("privmsg")
|
||||
@command("postpone")
|
||||
def postpone(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
|
||||
|
||||
is_already_in_queue, position = is_in_queue(bot, user, host)
|
||||
if is_already_in_queue:
|
||||
if position + 1 == len(bot.storage[key]):
|
||||
connection.notice(nick, "You are already at the end of the queue.")
|
||||
else:
|
||||
user = bot.storage[key].pop(position)
|
||||
bot.storage[key].insert(position + 1, user)
|
||||
connection.notice(nick, "You are now in the queue at position "
|
||||
"{}.".format(str(position + 2)))
|
||||
else:
|
||||
connection.notice(nick, "You are not in the queue. To enter it type "
|
||||
"!queue <speedtest URL>.")
|
||||
|
||||
|
||||
@event("privmsg")
|
||||
@command("cancel")
|
||||
def cancel(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
|
||||
|
||||
is_already_in_queue, position = is_in_queue(bot, user, host)
|
||||
if is_already_in_queue:
|
||||
bot.storage[key].pop(position)
|
||||
connection.notice(nick, "Successfully removed you from the queue.")
|
||||
else:
|
||||
connection.notice(nick, "You have not been added to the queue. To enter it type "
|
||||
"!queue <speedtest URL>.")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Admin module that helps to deal with other modules.
|
||||
"""
|
||||
from hermes.module import privmsg, command, admin_only
|
||||
from hermes.loader import load_module
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("list_modules")
|
||||
def list_modules(bot, connection, event):
|
||||
connection.privmsg(event.source.nick, "Loaded modules:")
|
||||
for mod in bot.modules:
|
||||
connection.privmsg(event.source.nick, "-> {}".format(mod))
|
||||
|
||||
|
||||
@admin_only()
|
||||
@privmsg()
|
||||
@command("reload")
|
||||
def reload_module(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
modules = [arg.lower() for arg in event.args]
|
||||
for mod in modules:
|
||||
if mod not in bot.modules:
|
||||
connection.privmsg(event.source.nick, "Unknown module: {}".format(mod))
|
||||
continue
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
load_module(bot.modules, bot.modules[mod].__mod_path__)
|
||||
if hasattr(bot.modules[mod], 'setup'):
|
||||
bot.modules[mod].setup(bot)
|
||||
bot.logger.info("-> Reloaded module: {}".format(mod))
|
||||
connection.privmsg(event.source.nick, "Reloaded module: {}".format(mod))
|
||||
except:
|
||||
bot.logger.exception("-> Failed to reload module: {}".format(mod))
|
||||
connection.privmsg(event.source.nick, "Failed to reload module: {}".format(mod))
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Module to allow people to ask to see the core stats of someone through pubmsg or privmsg. This
|
||||
will display someone's Upload, Download, Ratio, Bonus Points, and link to profile, assuming
|
||||
that their paranoia is set sufficiently low enough that it is viewable (we don't check to see
|
||||
if the requesting party has the right permissions to override paranoia if asking in a privmsg)
|
||||
"""
|
||||
|
||||
from hermes.module import event, command
|
||||
|
||||
|
||||
@event("pubmsg")
|
||||
@command("user", "u")
|
||||
def show_user(bot, connection, event):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
|
||||
chan = event.target.lstrip('#').lower()
|
||||
|
||||
if chan in bot.config.irc.channels:
|
||||
if 'min_level' not in bot.config.irc.channels[chan]:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
if len(event.args) == 0:
|
||||
# We can parse the username off the host string, but only if it's already been set by the
|
||||
# bot
|
||||
if not event.source.host.endswith(bot.config.site.tld):
|
||||
username = event.source.nick
|
||||
else:
|
||||
username = event.source.host.split(".")[0]
|
||||
else:
|
||||
username = event.args[0]
|
||||
if username is None:
|
||||
connection.privmsg(event.target, "No user found with name {}".format(username))
|
||||
return
|
||||
user = bot.database.get_user(username)
|
||||
if user is None or 'DisplayStats' not in user:
|
||||
connection.privmsg(event.target, "No user found with name {}".format(username))
|
||||
return
|
||||
|
||||
msg = "[ {} ] :: [ {} ] :: [ Uploaded: {} | Downloaded: {} | " \
|
||||
"Ratio: {} ] :: [ {} ]".format(user['Username'],
|
||||
user['ClassName'],
|
||||
user['DisplayStats']['Uploaded'],
|
||||
user['DisplayStats']['Downloaded'],
|
||||
user['DisplayStats']['Ratio'],
|
||||
user['UserPage'])
|
||||
connection.privmsg(event.target, msg)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
This module parses any youtube link posted in privmsg or pubmsg, responding (either in the
|
||||
privmsg or to the channel the link was posted on) with the name of the video, how many views it
|
||||
has gotten and then the link. Useful as most members don't like to have to click on links to
|
||||
see what it actually is.
|
||||
"""
|
||||
|
||||
import locale
|
||||
import re
|
||||
|
||||
import requests
|
||||
from hermes.module import event, rule, disabled
|
||||
|
||||
|
||||
@disabled()
|
||||
@rule(r"http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?",
|
||||
re.IGNORECASE)
|
||||
@event("privmsg", "pubmsg")
|
||||
def parse_youtube(bot, connection, event, match):
|
||||
"""
|
||||
|
||||
:param bot:
|
||||
:type bot: hermes.Hermes
|
||||
:param connection:
|
||||
:param event:
|
||||
:param match:
|
||||
:return:
|
||||
"""
|
||||
if not hasattr(bot.config, "youtube_api"):
|
||||
return
|
||||
if event.type == "privmsg":
|
||||
target = event.source.nick
|
||||
else:
|
||||
target = event.target
|
||||
|
||||
video_id = match.group(1)
|
||||
params = {'key': bot.config.youtube_api, 'id': video_id, 'maxResults': 1,
|
||||
'part': 'id,snippet,statistics'}
|
||||
req = requests.get("https://www.googleapis.com/youtube/v3/videos/", params)
|
||||
payload = req.json()
|
||||
if payload['pageInfo']['totalResults'] == 0:
|
||||
return "Youtube video not found!"
|
||||
video = payload['items'][0]
|
||||
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)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Stores data through reboots.
|
||||
"""
|
||||
|
||||
import pickle
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from .utils import DotDict
|
||||
|
||||
class PersistentStorage(object):
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
if os.path.isfile(path):
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
self.storage = pickle.load(f)
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
self.storage = DotDict()
|
||||
else:
|
||||
self.storage = DotDict()
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
with open(self.path, 'wb') as f:
|
||||
pickle.dump(self.storage, f)
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
pass
|
||||
|
||||
def __iter__(self):
|
||||
return self.storage.__iter__()
|
||||
|
||||
def __next__(self):
|
||||
return self.storage.__next__()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.store(key, value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.storage)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.storage
|
||||
|
||||
def store(self, key, value):
|
||||
self.storage[key] = value
|
||||
|
||||
def get(self, key):
|
||||
if key in self.storage:
|
||||
return self.storage[key]
|
||||
else:
|
||||
return None
|
||||
|
||||
def clear(self, key=None):
|
||||
if key:
|
||||
if key in self.storage:
|
||||
del self.storage[key]
|
||||
else:
|
||||
self.storage.clear()
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def calculate_size(size):
|
||||
"""
|
||||
Given some integer number of bytes, converts it to the largest possible unit till the number
|
||||
is somewhere between the range of [1, 1024).
|
||||
|
||||
:param size:
|
||||
:return: a human readable string for the size
|
||||
"""
|
||||
units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
unit = 0
|
||||
while size > 1024:
|
||||
size /= 1024
|
||||
unit += 1
|
||||
return str(round(size, 2 if unit < 4 else 4)) + " " + units[unit]
|
||||
|
||||
|
||||
def dict_to_list(d):
|
||||
"""Converts an ordered dict into a list."""
|
||||
# make sure it's a dict, that way dict_to_list can be used as an
|
||||
# array_hook.
|
||||
d = dict(d)
|
||||
try:
|
||||
return [str(d[x], 'utf-8') for x in range(len(d))]
|
||||
except KeyError:
|
||||
raise ValueError('dict is not a sequence')
|
||||
|
||||
|
||||
def run_popen(command):
|
||||
if isinstance(command, str):
|
||||
command = command.split()
|
||||
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
out, err = proc.communicate()
|
||||
return out, err
|
||||
|
||||
|
||||
def get_git_hash():
|
||||
"""
|
||||
This function attempts to get the hash of the head commit, assuming that hermes is being
|
||||
run out of a git repo, otherwise this will just return None
|
||||
|
||||
: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()
|
||||
return git_hash
|
||||
|
||||
|
||||
def check_pid(pid):
|
||||
"""
|
||||
This function checks if the given pid by sending it the 0 signal (which does nothing).
|
||||
However, when this function is run on windows, it will interpret the signal as
|
||||
signal.CTRL_C_EVENT which will kill the process, so we can really only use this on
|
||||
Unix.
|
||||
|
||||
:param pid:
|
||||
:return: boolean whether the process exists or not
|
||||
"""
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def load_config(config_file):
|
||||
"""Utility function to load the configuration file for Hermes and convert all dictionaries
|
||||
inside the config file to DotDict objects instead"""
|
||||
config = yaml.safe_load(open(config_file))
|
||||
return convert(config)
|
||||
|
||||
|
||||
def convert(node):
|
||||
"""Iterate through the node's items (if dict or list) converting them. All dicts are changed
|
||||
into DotDict objects"""
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
node[key] = convert(value)
|
||||
node = DotDict(node)
|
||||
elif isinstance(node, list):
|
||||
for elem in range(len(node)):
|
||||
node[elem] = convert(node[elem])
|
||||
return node
|
||||
|
||||
|
||||
class DotDict(dict):
|
||||
"""Utility class that allows for dot notation on dictionaries"""
|
||||
__getattr__ = dict.get
|
||||
__setattr__ = dict.__setitem__
|
||||
__delattr__ = dict.__delitem__
|
||||
__contains__ = dict.__contains__
|
||||
|
||||
def __getstate__(self):
|
||||
return self.__dict__
|
||||
def __setstate__(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
|
||||
def file_tail(file_name, num):
|
||||
"""
|
||||
Gets the last x number of lines from file_name. Basically the 'tail' unix command,
|
||||
but in python.
|
||||
|
||||
:param file_name:
|
||||
:param lines:
|
||||
:return:
|
||||
"""
|
||||
"""
|
||||
bufsize = 8192
|
||||
fsize = os.stat(file_name).st_size
|
||||
iterator = 0
|
||||
with open(file_name) as f:
|
||||
if bufsize > fsize:
|
||||
bufsize = fsize - 1
|
||||
data = []
|
||||
while True:
|
||||
iterator += 1
|
||||
f.seek(fsize - bufsize * iterator)
|
||||
data.extend(f.readlines())
|
||||
if len(data) >= lines or f.tell() == 0:
|
||||
return [x.strip() for x in data[-lines:]]
|
||||
return []
|
||||
"""
|
||||
lines = []
|
||||
with open(file_name) as f:
|
||||
for line in f:
|
||||
if len(lines) > num:
|
||||
lines.pop(0)
|
||||
lines.append(line)
|
||||
return lines
|
||||
@@ -0,0 +1,6 @@
|
||||
irc
|
||||
PyMySQL
|
||||
PyYAML
|
||||
requests
|
||||
sqlalchemy
|
||||
phpserialize
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
from hermes import run_hermes
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_hermes()
|
||||
@@ -0,0 +1,33 @@
|
||||
from setuptools import setup
|
||||
import sys
|
||||
|
||||
from hermes.hermes import __version__
|
||||
|
||||
if sys.version_info < (3, 5):
|
||||
raise SystemExit("hermes can only be installed using Python 3.5+")
|
||||
|
||||
setup(
|
||||
name='hermes',
|
||||
author="itismadness",
|
||||
version=__version__,
|
||||
description="An IRC bot designed to be used by sites running the Gazelle tracker software.",
|
||||
packages=['hermes'],
|
||||
entry_points={'console_scripts': ['hermes = hermes:run_hermes']},
|
||||
requires=[
|
||||
'irc',
|
||||
'phpserialize',
|
||||
'PyMySQL',
|
||||
'PyYAML',
|
||||
'requests',
|
||||
'sqlalchemy'
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Communications :: Chat :: Internet Relay Chat'
|
||||
]
|
||||
)
|
||||
Reference in new issue
Block a user