pidfile
logfile
oldlogs/
+erebus.db
---------------
- `cp bot.config.example bot.config`
- `vim bot.config`
-- Create a MySQL database, i.e. `CREATE DATABASE foo; GRANT ALL ON foo.* TO ...`
-- `mysql <dump.sql`
-- Add your auth (main NickServ nick/account name on other networks) in the database as owner: `INSERT INTO users VALUES ('YourAuth', 100);`
+- Pick a database:
+ - MySQL:
+ - Create a MySQL database, i.e. `CREATE DATABASE foo; GRANT ALL ON foo.* TO ...`
+ - `mysql <mysql.sql`
+ - dbtype=mysql, dbhost/dbuser/dbpass/dbname with connection info
+ - SQLite
+ - `sqlite3 erebus.db <sqlite.sql`
+ - dbtype=sqlite, dbhost=erebus.db (or other database path)
+- Populate at least the `bots` and `users` table:
+ - Add a bot: `INSERT INTO bots VALUES ('nick', 'user', NULL, NULL, NULL, 1, 0)`
+ - Add your auth (main NickServ nick/account name on other networks) in the database as owner: `INSERT INTO users VALUES ('YourAuth', 100);`
- `./run`
Install croncheck.sh in your crontab, if desired.
for u in chan.users:
if u.nick != self.nick:
self._clientLeft(u.nick, chan)
+ if chan.deleting:
+ chan.bot.chans.remove(chan)
+ del self.parent.chans[chan.name.lower()]
+ del chan
else:
user = self.parent.user(nick)
gone = user.part(chan)
from __future__ import print_function
-import os, sys, select, MySQLdb, MySQLdb.cursors, time, traceback, random, gc
-import bot, config, ctlmod
+import os, sys, select, time, traceback, random, gc
+import bot, config, ctlmod, modlib
class Erebus(object): #singleton to pass around
APIVERSION = 0
self.voices = []
self.ops = []
+ self.deleting = False # if true, the bot will remove cached records of this channel when the bot sees that it has left the channel
+
c = main.query("SELECT user, level FROM chusers WHERE chan = %s", (self.name,))
if c:
row = c.fetchone()
self.potype = "select"
self.fdlist = []
- def query(self, *args, **kwargs):
- if 'noretry' in kwargs:
- noretry = kwargs['noretry']
- del kwargs['noretry']
- else:
- noretry = False
+ def query(self, sql, parameters=[], noretry=False):
+ # Callers use %s-style (paramstyle='format') placeholders in queries.
+ # There's no provision for a literal '%s' present inside the query; stuff it in a parameter instead.
+ if db_api.paramstyle == 'format' or db_api.paramstyle == 'pyformat': # mysql, postgresql
+ # psycopg actually asks for a mapping with %(name)s style (pyformat) but it will accept %s style.
+ pass
+ elif db_api.paramstyle == 'qmark': # sqlite doesn't like %s style.
+ parameters = [str(p) for p in parameters]
+ sql = sql.replace('%s', '?') # hope that wasn't literal, oopsie
+
+ log_noretry = ''
+ if noretry:
+ log_noretry = ', noretry=True'
+ self.log("[SQL]", "?", "query(%r, %r%s)" % (sql, parameters, log_noretry))
- self.log("[SQL]", "?", "query(%s, %s)" % (', '.join([repr(i) for i in args]), ', '.join([str(key)+"="+repr(kwargs[key]) for key in kwargs])))
try:
curs = self.db.cursor()
- res = curs.execute(*args, **kwargs)
+ res = curs.execute(sql, parameters)
if res:
return curs
else:
return res
- except MySQLdb.DataError as e:
- self.log("[SQL]", ".", "MySQL DataError: %r" % (e))
+ except db_api.DataError as e:
+ self.log("[SQL]", ".", "DB DataError: %r" % (e))
return False
- except MySQLdb.MySQLError as e:
- self.log("[SQL]", "!", "MySQL error! %r" % (e))
+ except db_api.Error as e:
+ self.log("[SQL]", "!", "DB error! %r" % (e))
if not noretry:
dbsetup()
- return self.query(*args, noretry=True, **kwargs)
+ return self.query(sql, parameters, noretry=True)
else:
raise e
def querycb(self, cb, *args, **kwargs):
+ # TODO this should either get thrown out with getdb()/returndb(), or else be adjusted to make use of it.
def run_query():
cb(self.query(*args, **kwargs))
threading.Thread(target=run_query).start()
self.bots[nick.lower()] = obj
def newfd(self, obj, fileno):
+ if not isinstance(obj, modlib.Socketlike):
+ raise Exception('Attempted to hook a socket without a class to process data')
self.fds[fileno] = obj
if self.potype == "poll":
self.po.register(fileno, select.POLLIN)
return [u for u in self.users.values() if u.auth == auth.lower()]
def getdb(self):
- """Get a DB object. The object must be returned to the pool after us, using returndb()."""
+ """Get a DB object. The object must be returned to the pool after us, using returndb(). This is intended for use from child threads.
+ It should probably be treated as deprecated though. Where possible new modules should avoid using threads.
+ In the future, timers will be provided (manipulating the timeout_seconds of the poll() method), and that should mostly be used in place of threading."""
return self.dbs.pop()
def returndb(self, db):
def dbsetup():
main.db = None
main.dbs = []
+ dbtype = cfg.get('erebus', 'dbtype', 'mysql')
+ if dbtype == 'mysql':
+ _dbsetup_mysql()
+ elif dbtype == 'sqlite':
+ _dbsetup_sqlite()
+ else:
+ main.log('*', '!', 'Unknown dbtype in config: %s' % (dbtype))
+
+def _dbsetup_mysql():
+ global db_api
+ import MySQLdb as db_api, MySQLdb.cursors
for i in range(cfg.get('erebus', 'num_db_connections', 2)-1):
- main.dbs.append(MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor))
- main.db = MySQLdb.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)
+ main.dbs.append(db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor))
+ main.db = db_api.connect(host=cfg.dbhost, user=cfg.dbuser, passwd=cfg.dbpass, db=cfg.dbname, cursorclass=MySQLdb.cursors.DictCursor)
+
+def _dbsetup_sqlite():
+ global db_api
+ import sqlite3 as db_api
+ for i in range(cfg.get('erebus', 'num_db_connections', 2)):
+ main.db = db_api.connect(cfg.dbhost)
+ main.db.row_factory = db_api.Row
+ main.db.isolation_level = None
+ main.dbs.append(main.db)
def setup():
global cfg, main
try:
data = main.fd(fileno).getdata()
except:
- main.log('*', '!', 'Super-mega-emergency: getdata raised exception for socket %d' % (fileno))
+ main.log('*', '!', 'Error receiving data: getdata raised exception for socket %d, closing' % (fileno))
traceback.print_exc()
data = None
if data is None:
try:
main.fd(fileno).parse(line)
except:
- main.log('*', '!', 'Super-mega-emergency: parse raised exception for socket %d data %r' % (fileno, line))
+ main.log('*', '!', 'Error receiving data: parse raised exception for socket %d data %r, ignoring' % (fileno, line))
traceback.print_exc()
if main.mustquit is not None:
main.log('*', '!', 'Core exiting due to: %s' % (main.mustquit))
# module helper functions, see modules/modtest.py for usage
# This file is released into the public domain; see http://unlicense.org/
+import abc
import sys
import socket
from functools import wraps
WRONGARGS = "Wrong number of arguments."
def __init__(self, name):
+ self.Socketlike = Socketlike
self.hooks = {} # {command:handler}
self.chanhooks = {} # {channel:handler}
self.exceptionhooks = [] # [(exception,handler)]
return self._hooksocket(socket.AF_UNIX, socket.SOCK_STREAM, path, data)
def _hooksocket(self, af, ty, address, data):
def realhook(cls):
- if not (hasattr(cls, 'getdata') and callable(cls.getdata)):
- # Check early that the object implements getdata.
- # If getdata ever returns a non-empty list, then a parse method must also exist, but we don't check that.
+ if not issubclass(cls, Socketlike):
raise Exception('Attempted to hook a socket without a class to process data')
self.sockhooks.append((af, ty, address, cls, data))
if self.parent is not None:
return func
return realhook
-class _ListenSocket(object):
+class Socketlike(abc.ABC):
+ def __init__(self, sock, data):
+ """This default method saves the socket in self.sock and creates self.buffer for getdata(). The data is discarded."""
+ self.sock = sock
+ self.buffer = b''
+
+ def getdata(self):
+ """This default method gets LF or CRLF separated lines from the socket and returns an array of completely-seen lines to the core.
+ This should work well for most line-based protocols (like IRC)."""
+ recvd = self.sock.recv(8192)
+ if recvd == b"": # EOF
+ if len(self.buffer) != 0:
+ # Process what's left in the buffer. We'll get called again after.
+ remaining_buf = self.buffer.decode('utf-8', 'backslashreplace')
+ self.buffer = b""
+ return [remaining_buf]
+ else:
+ # Nothing left in the buffer. Return None to signal the core to close this socket.
+ return None
+ self.buffer += recvd
+ lines = []
+
+ while b"\n" in self.buffer:
+ pieces = self.buffer.split(b"\n", 1)
+ s = pieces[0].decode('utf-8', 'backslashreplace').rstrip("\r")
+ lines.append(pieces[0].decode('utf-8', 'backslashreplace'))
+ self.buffer = pieces[1]
+
+ return lines
+
+ def __str__(self):
+ return '%s#%d' % (self.__class__.__name__, self.sock.fileno())
+ def __repr__(self):
+ return '<%s.%s #%d %s:%d>' % ((self.__class__.__module__, self.__class__.__name__, self.sock.fileno())+self.sock.getpeername())
+
+ @abc.abstractmethod
+ def parse(self, chunk): pass
+
+ @classmethod
+ def __subclasshook__(cls, C):
+ if cls is Socketlike:
+ if any('parse' in B.__dict__ and 'getdata' in B.__dict__ for B in C.__mro__):
+ return True
+ return NotImplemented
+
+class _ListenSocket(Socketlike):
def __init__(self, lib, sock, cls, data):
self.clients = []
self.lib = lib
self.clients.remove((client,obj))
return close
+ def parse(self):
+ # getdata will never return a non-empty array, so parse will never be called; but Socketlike requires this method
+ pass
+
def getdata(self):
client, addr = self.sock.accept()
obj = self.cls(client, self.data)
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# simple module example
+# Channel list management (add, remove channels)
# This file is released into the public domain; see http://unlicense.org/
# module info
def _part(user, chan):
- chan.bot.chans.remove(chan)
- del lib.parent.chans[chan.name.lower()]
+ chan.deleting = True
lib.parent.query("DELETE FROM chusers WHERE chan = %s", (chan,))
lib.parent.query("DELETE FROM chans WHERE chname = %s", (chan,))
chan.bot.part(chan)
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# simple module example
+# Commands to change config file settings
# This file is released into the public domain; see http://unlicense.org/
# module info
# module code
-def _resolve_user(s):
- if s.startswith("#"):
- return lib.parent.User(s, s[1:])
+@lib.hook(needchan=False, glevel=lib.OWNER)
+@lib.help('<section> <key>', 'gets a current config file setting')
+@lib.argsEQ(2)
+def getconfig(bot, user, chan, realtarget, *args):
+ section, key = args[0], args[1]
+ value = bot.parent.cfg.get(section, key)
+ if value is not None:
+ user.msg('[%s] %s: %s' % (section, key, value))
else:
- return lib.parent.user(s, create=False)
-
-def _resolve_level(s):
- try:
- i = int(s)
- return i
- except ValueError:
- su = s.upper()
- if su == "ANYONE":
- su = "AUTHED" # resolve to 0 instead of -1
- if su in lib.glevs:
- return lib.glevs[su]
- return None
+ user.msg('That option is not set ([%s] %s)' % (section, key))
@lib.hook(needchan=False, glevel=lib.OWNER)
@lib.help('<section> <key> <value>', 'sets a config file setting to a new value')
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# simple module example
+# Global-user management (glevel)
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# simple module example
+# Channel-access management
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# !EVAL and !EXEC commands
+# !EVAL and !EXEC commands, dangerous! DO NOT USE without understanding the risks!
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# Various highly recommended "control" commands.
+# An example of the "flags" capability
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# A basic exception hook. Sends exceptions to `[exception_hook] destination` in the config.
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# ListenBrainz now-playing
# module info
modinfo = {
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# simple module example
+# assorted simple functions
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# Commands to message various destinations
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# module for 's/regex/replacement/' style correction
-# warning: arbitrary regex's are generally capable of various DoS attacks on CPU/memory usage. use with caution.
-# see for usage examples: https://github.com/zonidjan/erebus/commit/d7e9802778477f1faa26a03078cb1b3c018a5e5c
+# nitterize twitter links
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# resource-usage module
+# resource-usage reporting module
# This file is released into the public domain; see http://unlicense.org/
# module info
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# Utility commands to show info like uptime and current server
# This file is released into the public domain; see http://unlicense.org/
import time
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# Configurable sockets module. DO NOT USE without understanding the risks
# This file is released into the public domain; see http://unlicense.org/
# Note: this module doesn't do any kind of authentication, so anyone who can connect to the bound port can spam you.
+"""
+To use - add in bot.config something like:
+
+[sockets]
+127.0.0.1:1337 = #example
+
+The left side is the address to listen on and the right side is the channel to send to.
+The exmaple will send incoming lines/packets on localhost, port 1337 to channel #example
+
+The full syntax for the address is:
+[unix:]</path/to/socket>
+[udp|tcp:][<ip>:]<port>
+
+
+Address examples:
+
+Unix domain socket: /path
+Unix domain socket: unix:/path
+TCP socket (all interfaces): 1337
+TCP socket (one interface): 127.0.0.1:1337
+UDP socket (all interfaces): udp:1337
+UDP socket (one interface): udp:127.0.0.1:1337
+"""
+
# module info
modinfo = {
'author': 'Erebus Team',
modstop = lib.modstop
# module code
+class BasicServer(lib.Socketlike):
+ def __init__(self, sock, data):
+ super(BasicServer, self).__init__(sock, data)
+ # NB neither directly referencing `channel`, nor trying to pass it through a default-arg-to-a-lambda like the python docs suggest, works here.
+ # Yay python. At least passing it via bind works.
+ self.chan = data
-def gotParent(parent):
- for bindto, channel in parent.cfg.items('sockets'):
- @lib.bind(bindto, data=channel)
- class BasicServer(object):
- def __init__(self, sock, data):
- # NB neither directly referencing `channel`, nor trying to pass it through a default-arg-to-a-lambda like the python docs suggest, works here.
- # Yay python. At least passing it via bind works.
- self.chan = data
- self.buffer = b''
- self.sock = sock
-
- def getdata(self):
- recvd = self.sock.recv(8192)
- if recvd == b"": # EOF
- if len(self.buffer) != 0:
- # Process what's left in the buffer. We'll get called again after.
- remaining_buf = self.buffer.decode('utf-8', 'backslashreplace')
- self.buffer = b""
- return [remaining_buf]
- else:
- # Nothing left in the buffer. Return None to signal the core to close this socket.
- return None
- self.buffer += recvd
- lines = []
-
- while b"\n" in self.buffer:
- pieces = self.buffer.split(b"\n", 1)
- s = pieces[0].decode('utf-8', 'backslashreplace').rstrip("\r")
- lines.append(pieces[0].decode('utf-8', 'backslashreplace'))
- self.buffer = pieces[1]
-
- return lines
+ # default getdata() and send() methods are defined by lib.Socketlike
+ # suitable for line-based protocols like IRC
- def parse(self, line):
- try:
- bot = lib.parent.channel(self.chan).bot
- except AttributeError: # <class 'AttributeError'> 'NoneType' object has no attribute 'bot'
- bot = lib.parent.randbot()
- maxlen = bot.maxmsglen() - len("PRIVMSG :") - len(self.chan)
- while len(line) > maxlen:
- cutat = line.rfind(' ', 0, maxlen)
- if cutat == -1:
- cutat = maxlen
- bot.msg(self.chan, line[0:cutat])
- line = line[cutat:].strip()
- bot.msg(self.chan, line)
+ def parse(self, line):
+ try:
+ bot = lib.parent.channel(self.chan).bot
+ except AttributeError: # <class 'AttributeError'> 'NoneType' object has no attribute 'bot'
+ bot = lib.parent.randbot()
+ maxlen = bot.maxmsglen() - len("PRIVMSG :") - len(self.chan)
+ while len(line) > maxlen:
+ cutat = line.rfind(' ', 0, maxlen)
+ if cutat == -1:
+ cutat = maxlen
+ bot.msg(self.chan, line[0:cutat])
+ line = line[cutat:].strip()
+ bot.msg(self.chan, line)
- def send(self, line):
- if lib.parent.parent.cfg.getboolean('debug', 'io'):
- lib.parent.log(str(self), 'O', line)
- self.sock.sendall(line.encode('utf-8', 'backslashreplace')+b"\r\n")
+ # default __str__() and __repr__() methods are defined by lib.Socketlike
- def _getsockerr(self):
- try: # SO_ERROR might not exist on all platforms
- return self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
- except:
- return None
-
- def __str__(self):
- return '%s#%d' % (__name__, self.sock.fileno())
- def __repr__(self):
- return '<%s #%d %s:%d>' % ((__name__, self.sock.fileno())+self.sock.getpeername())
+def gotParent(parent):
+ for bindto, channel in parent.cfg.items('sockets'):
+ lib.bind(bindto, data=channel)(BasicServer)
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
+# Steam API commands (!steamnp)
# module info
modinfo = {
# vim: fileencoding=utf-8
# module for 's/regex/replacement/' style correction
# warning: arbitrary regex's are generally capable of various DoS attacks on CPU/memory usage. use with caution.
-# see for usage examples: https://github.com/zonidjan/erebus/commit/d7e9802778477f1faa26a03078cb1b3c018a5e5c
# This file is released into the public domain; see http://unlicense.org/
# module info
"""
try:
request = urllib2.Request(url, headers={
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
- 'Sec-Ch-Ua': '"Chromium";v="116", "Not)A;Brand";v="24", "Google Chrome";v="116"',
- 'Sec-Ch-Ua-Mobile': '?0',
- 'Sec-Ch-Ua-Platform': '"Linux"',
- 'Sec-Fetch-Dest': 'document',
- 'Sec-Fetch-Mode': 'navigate',
- 'Sec-Fetch-Site': 'same-origin',
- 'Sec-Fetch-User': '?1',
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
- 'Accept-Language': 'en-US,en;q=0.9',
- 'Upgrade-Insecure-Requests': '1'
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
+ 'accept-language': 'en-US,en;q=0.9',
+ 'cache-control': 'max-age=0',
+ 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
+ 'sec-ch-ua-mobile': '?0',
+ 'sec-ch-ua-platform': '"Linux"',
+ 'sec-fetch-dest': 'document',
+ 'sec-fetch-mode': 'navigate',
+ 'sec-fetch-site': 'none',
+ 'sec-fetch-user': '?1',
+ 'upgrade-insecure-requests': '1',
+ 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
})
except ValueError:
return '', False
# Erebus IRC bot - Author: Erebus Team
# vim: fileencoding=utf-8
-# weather module
+# weather module (from WeatherStack, boo)
# This file is released into the public domain; see http://unlicense.org/
# module info
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
---
--- Dumping data for table `bots`
---
-
-LOCK TABLES `bots` WRITE;
-/*!40000 ALTER TABLE `bots` DISABLE KEYS */;
-INSERT INTO `bots` VALUES
-('Erebus','Erebus',NULL,'Erebus','',1,0);
-/*!40000 ALTER TABLE `bots` ENABLE KEYS */;
-UNLOCK TABLES;
-
--
-- Table structure for table `chans`
--
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
---
--- Dumping data for table `chans`
---
-
-LOCK TABLES `chans` WRITE;
-/*!40000 ALTER TABLE `chans` DISABLE KEYS */;
-INSERT INTO `chans` VALUES
-('#','Erebus',1);
-/*!40000 ALTER TABLE `chans` ENABLE KEYS */;
-UNLOCK TABLES;
-
--
-- Table structure for table `chusers`
--
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
---
--- Dumping data for table `chusers`
---
-
-LOCK TABLES `chusers` WRITE;
-/*!40000 ALTER TABLE `chusers` DISABLE KEYS */;
-INSERT INTO `chusers` VALUES
-('#','dimecadmium',5);
-/*!40000 ALTER TABLE `chusers` ENABLE KEYS */;
-UNLOCK TABLES;
-
--
-- Table structure for table `users`
--
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
---
--- Dumping data for table `users`
---
-
-LOCK TABLES `users` WRITE;
-/*!40000 ALTER TABLE `users` DISABLE KEYS */;
-INSERT INTO `users` VALUES
-('dimecadmium',100);
-/*!40000 ALTER TABLE `users` ENABLE KEYS */;
-UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
--- /dev/null
+CREATE TABLE `bots` (
+ `nick` varchar(30) NOT NULL,
+ `user` varchar(10) NOT NULL DEFAULT 'erebus',
+ `bind` varchar(15) DEFAULT NULL,
+ `authname` varchar(30) DEFAULT NULL,
+ `authpass` varchar(20) DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT 1,
+ `connected` tinyint(1) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`nick`)
+);
+CREATE TABLE `chans` (
+ `chname` varchar(100) NOT NULL,
+ `bot` varchar(30) NOT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT 1,
+ PRIMARY KEY (`chname`)
+);
+CREATE INDEX chans_bot ON chans (bot);
+CREATE TABLE `chusers` (
+ `chan` varchar(100) NOT NULL,
+ `user` varchar(30) NOT NULL,
+ `level` int(11) NOT NULL,
+ PRIMARY KEY (`chan`,`user`)
+);
+CREATE TABLE `users` (
+ `auth` varchar(30) NOT NULL,
+ `level` tinyint(3) NOT NULL,
+ PRIMARY KEY (`auth`)
+);