SpiceBot-migrated/sopel_modules/SpiceBot/Database.py
2019-07-01 16:23:51 -04:00

693 lines
26 KiB
Python

# coding=utf8
from __future__ import unicode_literals, absolute_import, division, print_function
"""
This is the SpiceBot Database
"""
# sopel imports
from sopel.tools import Identifier
import sopel.db
from sopel.db import SopelDB, _deserialize
from .Config import config as botconfig
from sqlalchemy.engine.url import URL
from sqlalchemy import create_engine, Column, String, ForeignKey, Integer, Unicode
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.exc import SQLAlchemyError
import json
import threading
import os
BASE = declarative_base()
class NickIDs(BASE):
"""
NickIDs SQLAlchemy Class
"""
__tablename__ = 'spice_nick_ids'
nick_id = Column(Integer, primary_key=True)
class Nicknames(BASE):
"""
Nicknames SQLAlchemy Class
"""
__tablename__ = 'spice_nicknames'
nick_id = Column(Integer, ForeignKey('spice_nick_ids.nick_id'), primary_key=True)
slug = Column(String(255), primary_key=True)
canonical = Column(String(255))
class NickValues(BASE):
"""
NickValues SQLAlchemy Class
"""
__tablename__ = 'spice_nick_values'
nick_id = Column(Integer, ForeignKey('spice_nick_ids.nick_id'), primary_key=True)
namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True)
value = Column(Unicode())
class ChannelValues(BASE):
"""
ChannelValues SQLAlchemy Class
"""
__tablename__ = 'spice_channel_values'
channel = Column(String(255), primary_key=True)
namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True)
value = Column(Unicode())
class PluginValues(BASE):
"""
PluginValues SQLAlchemy Class
"""
__tablename__ = 'spice_plugin_values'
plugin = Column(String(255), primary_key=True)
namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True)
value = Column(Unicode())
class SpiceDB(object):
# NICK FUNCTIONS
def get_nick_id(self, nick, create=True):
"""Return the internal identifier for a given nick.
This identifier is unique to a user, and shared across all of that
user's aliases. If create is True, a new ID will be created if one does
not already exist"""
session = self.ssession()
slug = nick.lower()
try:
self.nick_id_lock.acquire()
nickname = session.query(Nicknames) \
.filter(Nicknames.slug == slug) \
.one_or_none()
if nickname is None:
if not create:
self.nick_id_lock.release()
raise ValueError('No ID exists for the given nick')
# Generate a new ID
nick_id = NickIDs()
session.add(nick_id)
session.commit()
# Create a new Nickname
nickname = Nicknames(nick_id=nick_id.nick_id, slug=slug, canonical=nick)
session.add(nickname)
session.commit()
self.nick_id_lock.release()
return nickname.nick_id
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def set_nick_value(self, nick, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the nick."""
nick = Identifier(nick)
value = json.dumps(value, ensure_ascii=False)
nick_id = self.get_nick_id(nick)
session = self.ssession()
try:
result = session.query(NickValues) \
.filter(NickValues.nick_id == nick_id) \
.filter(NickValues.namespace == namespace)\
.filter(NickValues.key == key) \
.one_or_none()
# NickValue exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_nickvalue = NickValues(nick_id=nick_id, namespace=namespace, key=key, value=value)
session.add(new_nickvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_nick_value(self, nick, key, namespace='default'):
"""Retrieves the value for a given key associated with a nick."""
nick = Identifier(nick)
session = self.ssession()
try:
result = session.query(NickValues) \
.filter(Nicknames.nick_id == NickValues.nick_id) \
.filter(Nicknames.slug == nick.lower()) \
.filter(NickValues.namespace == namespace)\
.filter(NickValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_nick_value(self, nick, key, namespace='default'):
"""Deletes the value for a given key to be associated with the nick."""
nick = Identifier(nick)
nick_id = self.get_nick_id(nick)
session = self.ssession()
try:
result = session.query(NickValues) \
.filter(NickValues.nick_id == nick_id) \
.filter(NickValues.namespace == namespace)\
.filter(NickValues.key == key) \
.one_or_none()
# NickValue exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_nick_value(self, nick, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the nick."""
nick = Identifier(nick)
value = json.dumps(value, ensure_ascii=False)
nick_id = self.get_nick_id(nick)
session = self.ssession()
try:
result = session.query(NickValues) \
.filter(NickValues.nick_id == nick_id) \
.filter(NickValues.namespace == namespace)\
.filter(NickValues.key == key) \
.one_or_none()
# NickValue exists, update
if result:
result.value = float(result.value) + float(value)
session.commit()
# DNE - Insert
else:
new_nickvalue = NickValues(nick_id=nick_id, namespace=namespace, key=key, value=float(value))
session.add(new_nickvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_nick_list(self, nick, key, entries, adjustmentdirection, namespace='default'):
"""Sets the value for a given key to be associated with the nick."""
nick = Identifier(nick)
if not isinstance(entries, list):
entries = [entries]
entries = json.dumps(entries, ensure_ascii=False)
nick_id = self.get_nick_id(nick)
session = self.ssession()
try:
result = session.query(NickValues) \
.filter(NickValues.nick_id == nick_id) \
.filter(NickValues.namespace == namespace)\
.filter(NickValues.key == key) \
.one_or_none()
# NickValue exists, update
if result:
if adjustmentdirection == 'add':
for entry in entries:
if entry not in result.value:
result.value.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in result.value:
result.value.remove(entry)
session.commit()
# DNE - Insert
else:
values = []
if adjustmentdirection == 'add':
for entry in entries:
if entry not in values:
values.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in values:
values.remove(entry)
new_nickvalue = NickValues(nick_id=nick_id, namespace=namespace, key=key, value=values)
session.add(new_nickvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
# CHANNEL FUNCTIONS
def set_channel_value(self, channel, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the channel."""
channel = Identifier(channel).lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValue exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_channelvalue = ChannelValues(channel=channel, namespace=namespace, key=key, value=value)
session.add(new_channelvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_channel_value(self, channel, key, namespace='default'):
"""Retrieves the value for a given key associated with a channel."""
channel = Identifier(channel).lower()
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_channel_value(self, channel, key, namespace='default'):
"""Sets the value for a given key to be associated with the channel."""
channel = Identifier(channel).lower()
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValue exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_channel_value(self, channel, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the channel."""
channel = Identifier(channel).lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValue exists, update
if result:
result.value = float(result.value) + float(value)
session.commit()
# DNE - Insert
else:
new_channelvalue = ChannelValues(channel=channel, namespace=namespace, key=key, value=float(value))
session.add(new_channelvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_channel_list(self, channel, key, entries, adjustmentdirection, namespace='default'):
"""Sets the value for a given key to be associated with the channel."""
channel = Identifier(channel).lower()
if not isinstance(entries, list):
entries = [entries]
entries = json.dumps(entries, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValue exists, update
if result:
if adjustmentdirection == 'add':
for entry in entries:
if entry not in result.value:
result.value.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in result.value:
result.value.remove(entry)
session.commit()
# DNE - Insert
else:
values = []
if adjustmentdirection == 'add':
for entry in entries:
if entry not in values:
values.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in values:
values.remove(entry)
new_channelvalue = ChannelValues(channel=channel, namespace=namespace, key=key, value=values)
session.add(new_channelvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
# PLUGIN FUNCTIONS
def set_plugin_value(self, plugin, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the plugin."""
plugin = Identifier(plugin).lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.plugin == plugin)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# PluginValues exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_pluginvalue = PluginValues(plugin=plugin, namespace=namespace, key=key, value=value)
session.add(new_pluginvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_plugin_value(self, plugin, key, namespace='default'):
"""Retrieves the value for a given key associated with a plugin."""
plugin = Identifier(plugin).lower()
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.plugin == plugin)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_plugin_value(self, plugin, key, namespace='default'):
"""Deletes the value for a given key to be associated with the plugin."""
plugin = Identifier(plugin).lower()
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.plugin == plugin)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# PluginValues exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_plugin_value(self, plugin, key, value, namespace='default'):
"""Sets the value for a given key to be associated with the plugin."""
plugin = Identifier(plugin).lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.plugin == plugin)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# ChannelValue exists, update
if result:
result.value = float(result.value) + float(value)
session.commit()
# DNE - Insert
else:
new_pluginvalue = PluginValues(plugin=plugin, namespace=namespace, key=key, value=float(value))
session.add(new_pluginvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def adjust_plugin_list(self, plugin, key, entries, adjustmentdirection, namespace='default'):
"""Sets the value for a given key to be associated with the plugin."""
plugin = Identifier(plugin).lower()
if not isinstance(entries, list):
entries = [entries]
entries = json.dumps(entries, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.plugin == plugin)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# ChannelValue exists, update
if result:
if adjustmentdirection == 'add':
for entry in entries:
if entry not in result.value:
result.value.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in result.value:
result.value.remove(entry)
session.commit()
# DNE - Insert
else:
values = []
if adjustmentdirection == 'add':
for entry in entries:
if entry not in values:
values.append(entry)
elif adjustmentdirection == 'del':
for entry in entries:
while entry in values:
values.remove(entry)
new_pluginvalue = PluginValues(plugin=plugin, namespace=namespace, key=key, value=values)
session.add(new_pluginvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
class BotDatabase():
def __init__(self):
db_type = botconfig.core.db_type
if db_type == 'sqlite':
path = botconfig.core.db_filename
config_dir, config_file = os.path.split(botconfig.filename)
config_name, _ = os.path.splitext(config_file)
if path is None:
path = os.path.join(config_dir, config_name + '.db')
path = os.path.expanduser(path)
if not os.path.isabs(path):
path = os.path.normpath(os.path.join(config_dir, path))
SopelDB.filename = path
SopelDB.url = 'sqlite:///%s' % path
# Otherwise, handle all other database engines
else:
if db_type == 'mysql':
drivername = botconfig.core.db_driver or 'mysql'
elif db_type == 'postgres':
drivername = botconfig.core.db_driver or 'postgresql'
elif db_type == 'oracle':
drivername = botconfig.core.db_driver or 'oracle'
elif db_type == 'mssql':
drivername = botconfig.core.db_driver or 'mssql+pymssql'
elif db_type == 'firebird':
drivername = botconfig.core.db_driver or 'firebird+fdb'
elif db_type == 'sybase':
drivername = botconfig.core.db_driver or 'sybase+pysybase'
else:
raise Exception('Unknown db_type')
db_user = botconfig.core.db_user
db_pass = botconfig.core.db_pass
db_host = botconfig.core.db_host
db_port = botconfig.core.db_port # Optional
db_name = botconfig.core.db_name # Optional, depending on DB
SopelDB.url = URL(drivername=drivername, username=db_user, password=db_pass,
host=db_host, port=db_port, database=db_name, query={'charset': 'utf8'})
SopelDB.engine = create_engine(SopelDB.url, encoding='utf-8', convert_unicode=True)
SopelDB.nick_id_lock = threading.Lock()
sopel.db.NickIDs = NickIDs
sopel.db.Nicknames = Nicknames
sopel.db.NickValues = NickValues
SopelDB.get_nick_id = SpiceDB.get_nick_id
SopelDB.get_nick_value = SpiceDB.get_nick_value
SopelDB.set_nick_value = SpiceDB.set_nick_value
SopelDB.delete_nick_value = SpiceDB.delete_nick_value
SopelDB.adjust_nick_value = SpiceDB.adjust_nick_value
SopelDB.adjust_nick_list = SpiceDB.adjust_nick_list
sopel.db.ChannelValues = ChannelValues
SopelDB.get_channel_value = SpiceDB.get_channel_value
SopelDB.set_channel_value = SpiceDB.set_channel_value
SopelDB.delete_channel_value = SpiceDB.delete_channel_value
SopelDB.adjust_channel_value = SpiceDB.adjust_channel_value
SopelDB.adjust_channel_list = SpiceDB.adjust_channel_list
sopel.db.PluginValues = PluginValues
SopelDB.get_plugin_value = SpiceDB.get_plugin_value
SopelDB.set_plugin_value = SpiceDB.set_plugin_value
SopelDB.delete_plugin_value = SpiceDB.delete_plugin_value
SopelDB.adjust_plugin_value = SpiceDB.adjust_plugin_value
SopelDB.adjust_plugin_list = SpiceDB.adjust_plugin_list
self.db = SopelDB(botconfig.config)
BASE.metadata.create_all(self.db.engine)
"""Nick"""
def get_nick_id(self, nick, create=True):
return self.db.get_nick_id(nick, create)
def alias_nick(self, nick, alias):
return self.db.alias_nick(nick, alias)
def unalias_nick(self, alias):
return self.db.unalias_nick(alias)
def check_nick_id(self, nick):
try:
self.db.get_nick_id(nick, create=False)
return True
except ValueError:
return False
def get_nick_value(self, nick, key, namespace='default'):
return self.db.get_nick_value(nick, key, namespace)
def set_nick_value(self, nick, key, value, namespace='default'):
return self.db.set_nick_value(nick, key, value, namespace)
def delete_nick_value(self, nick, key, namespace='default'):
return self.db.delete_nick_value(nick, key, namespace)
def adjust_nick_value(self, nick, key, value, namespace='default'):
return self.db.adjust_nick_value(nick, key, value, namespace)
def adjust_nick_list(self, nick, key, entries, adjustmentdirection, namespace='default'):
return self.db.adjust_nick_list(nick, key, entries, adjustmentdirection, namespace)
"""Bot"""
def get_bot_value(self, key, namespace='default'):
return self.db.get_nick_value(botconfig.nick, key, namespace)
def set_bot_value(self, key, value, namespace='default'):
return self.db.set_nick_value(botconfig.nick, key, value, namespace)
def delete_bot_value(self, key, namespace='default'):
return self.db.delete_nick_value(botconfig.nick, key, namespace)
def adjust_bot_value(self, key, value, namespace='default'):
return self.db.adjust_nick_value(botconfig.nick, key, value, namespace)
def adjust_bot_list(self, key, entries, adjustmentdirection, namespace='default'):
return self.db.adjust_nick_list(botconfig.nick, key, entries, adjustmentdirection, namespace)
"""Channels"""
def get_channel_value(self, channel, key, namespace='default'):
return self.db.get_channel_value(channel, key, namespace)
def set_channel_value(self, channel, key, value, namespace='default'):
return self.db.set_channel_value(channel, key, value, namespace)
def delete_channel_value(self, channel, key, namespace='default'):
return self.db.delete_channel_value(channel, key, namespace)
def adjust_channel_value(self, channel, key, value, namespace='default'):
return self.db.adjust_channel_value(channel, key, value, namespace)
def adjust_channel_list(self, nick, key, entries, adjustmentdirection, namespace='default'):
return self.db.adjust_channel_list(nick, key, entries, adjustmentdirection, namespace)
"""Plugins"""
def get_plugin_value(self, plugin, key, namespace='default'):
return self.db.get_plugin_value(plugin, key, namespace)
def set_plugin_value(self, plugin, key, value, namespace='default'):
return self.db.set_plugin_value(plugin, key, value, namespace)
def delete_plugin_value(self, plugin, key, namespace='default'):
return self.db.delete_plugin_value(plugin, key, namespace)
def adjust_plugin_value(self, plugin, key, value, namespace='default'):
return self.db.adjust_plugin_value(plugin, key, value, namespace)
def adjust_plugin_list(self, nick, key, entries, adjustmentdirection, namespace='default'):
return self.db.adjust_plugin_list(nick, key, entries, adjustmentdirection, namespace)
db = BotDatabase()