some refactoring, removed some useless stuff, added some useless stuff
This commit is contained in:
parent
04cc2a2084
commit
e6e8d544d2
32
bot.py
32
bot.py
|
@ -1,12 +1,8 @@
|
|||
# coding=utf-8
|
||||
# Copyright 2008, Sean B. Palmer, inamidst.com
|
||||
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
||||
# Copyright 2012-2015, Elsie Powell, http://embolalia.com
|
||||
#
|
||||
# Licensed under the Eiffel Forum License 2.
|
||||
|
||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||||
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The core bot class. Say good bye to PYthon 2.
|
||||
"""
|
||||
import collections
|
||||
import os
|
||||
import re
|
||||
|
@ -27,13 +23,6 @@ import loader
|
|||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
if sys.version_info.major >= 3:
|
||||
unicode = str
|
||||
basestring = str
|
||||
py3 = True
|
||||
else:
|
||||
py3 = False
|
||||
|
||||
|
||||
class _CapReq(object):
|
||||
def __init__(self, prefix, module, failure=None, arg=None, success=None):
|
||||
|
@ -70,7 +59,6 @@ class Sopel(irc.Bot):
|
|||
"""
|
||||
self._command_groups = collections.defaultdict(list)
|
||||
"""A mapping of module names to a list of commands in it."""
|
||||
self.stats = {} # deprecated, remove in 7.0
|
||||
self._times = {}
|
||||
"""
|
||||
A dictionary mapping lower-case'd nicks to dictionaries which map
|
||||
|
@ -261,10 +249,6 @@ class Sopel(irc.Bot):
|
|||
else:
|
||||
self.write(['JOIN', channel, password])
|
||||
|
||||
def msg(self, recipient, text, max_messages=1):
|
||||
# Deprecated, but way too much of a pain to remove.
|
||||
self.say(text, recipient, max_messages)
|
||||
|
||||
def say(self, text, recipient, max_messages=1):
|
||||
"""Send ``text`` as a PRIVMSG to ``recipient``.
|
||||
|
||||
|
@ -288,7 +272,7 @@ class Sopel(irc.Bot):
|
|||
max_text_length = 400
|
||||
max_messages=1000
|
||||
# Encode to bytes, for propper length calculation
|
||||
if isinstance(text, unicode):
|
||||
if isinstance(text, str):
|
||||
encoded_text = text.encode('utf-8')
|
||||
else:
|
||||
encoded_text = text
|
||||
|
@ -306,7 +290,7 @@ class Sopel(irc.Bot):
|
|||
excess = encoded_text[last_space + 1:]
|
||||
encoded_text = encoded_text[:last_space]
|
||||
# We'll then send the excess at the end
|
||||
# Back to unicode again, so we don't screw things up later.
|
||||
# Back to str again, so we don't screw things up later.
|
||||
text = encoded_text.decode('utf-8')
|
||||
try:
|
||||
self.sending.acquire()
|
||||
|
@ -327,7 +311,7 @@ class Sopel(irc.Bot):
|
|||
# Now that we've sent the first part, we need to send the rest. Doing
|
||||
# this recursively seems easier to me than iteratively
|
||||
if excess:
|
||||
self.msg(recipient, excess, max_messages - 1)
|
||||
self.say(recipient, excess, max_messages - 1)
|
||||
|
||||
def notice(self, text, dest):
|
||||
"""Send an IRC NOTICE to a user or a channel.
|
||||
|
|
|
@ -37,10 +37,7 @@ def auth_after_register(bot):
|
|||
"""Do NickServ/AuthServ auth"""
|
||||
if bot.config.core.auth_method == 'nickserv':
|
||||
nickserv_name = bot.config.core.auth_target or 'NickServ'
|
||||
bot.msg(
|
||||
nickserv_name,
|
||||
'IDENTIFY %s' % bot.config.core.auth_password
|
||||
)
|
||||
bot.say('IDENTIFY %s' % bot.config.core.auth_password, nickserv_name)
|
||||
|
||||
elif bot.config.core.auth_method == 'authserv':
|
||||
account = bot.config.core.auth_username
|
||||
|
@ -107,7 +104,7 @@ def startup(bot, trigger):
|
|||
"more secure. If you'd like to do this, make sure you're logged in "
|
||||
"and reply with \"{}useserviceauth\""
|
||||
).format(bot.config.core.help_prefix)
|
||||
bot.msg(bot.config.core.owner, msg)
|
||||
bot.say(msg, bot.config.core.owner)
|
||||
|
||||
|
||||
@module.require_privmsg()
|
||||
|
@ -262,7 +259,7 @@ def track_nicks(bot, trigger):
|
|||
debug_msg = ("Nick changed by server. "
|
||||
"This can cause unexpected behavior. Please restart the bot.")
|
||||
LOGGER.critical(debug_msg)
|
||||
bot.msg(bot.config.core.owner, privmsg)
|
||||
bot.say(privmsg, bot.config.core.owner)
|
||||
return
|
||||
|
||||
for channel in bot.privileges:
|
||||
|
|
88
irc.py
88
irc.py
|
@ -1,43 +1,24 @@
|
|||
# coding=utf-8
|
||||
# irc.py - An Utility IRC Bot
|
||||
# Copyright 2008, Sean B. Palmer, inamidst.com
|
||||
# Copyright 2012, Elsie Powell, http://embolalia.com
|
||||
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
|
||||
#
|
||||
# Licensed under the Eiffel Forum License 2.
|
||||
#
|
||||
# When working on core IRC protocol related features, consult protocol
|
||||
# documentation at http://www.irchelp.org/irchelp/rfc/
|
||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||||
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Core IRC functionality. Support for Python 2 is largely gone.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import socket
|
||||
import asyncore
|
||||
import asynchat
|
||||
import os
|
||||
import codecs
|
||||
import traceback
|
||||
import threading
|
||||
from datetime import datetime
|
||||
import errno
|
||||
import ssl
|
||||
import asyncore
|
||||
import asynchat
|
||||
|
||||
from logger import get_logger
|
||||
from tools import stderr, Identifier
|
||||
from trigger import PreTrigger
|
||||
try:
|
||||
import ssl
|
||||
if not hasattr(ssl, 'match_hostname'):
|
||||
# Attempt to import ssl_match_hostname from python-backports
|
||||
import backports.ssl_match_hostname
|
||||
ssl.match_hostname = backports.ssl_match_hostname.match_hostname
|
||||
ssl.CertificateError = backports.ssl_match_hostname.CertificateError
|
||||
has_ssl = True
|
||||
except ImportError:
|
||||
# no SSL support
|
||||
has_ssl = False
|
||||
|
||||
import errno
|
||||
import threading
|
||||
from datetime import datetime
|
||||
if sys.version_info.major >= 3:
|
||||
unicode = str
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
@ -67,17 +48,6 @@ class Bot(asynchat.async_chat):
|
|||
self.writing_lock = threading.Lock()
|
||||
self.raw = None
|
||||
|
||||
# Right now, only accounting for two op levels.
|
||||
# This might be expanded later.
|
||||
# These lists are filled in startup.py, as of right now.
|
||||
# Are these even touched at all anymore? Remove in 7.0.
|
||||
self.ops = dict()
|
||||
"""Deprecated. Use bot.channels instead."""
|
||||
self.halfplus = dict()
|
||||
"""Deprecated. Use bot.channels instead."""
|
||||
self.voices = dict()
|
||||
"""Deprecated. Use bot.channels instead."""
|
||||
|
||||
# We need this to prevent error loops in handle_error
|
||||
self.error_count = 0
|
||||
|
||||
|
@ -85,10 +55,6 @@ class Bot(asynchat.async_chat):
|
|||
""" Set to True when a server has accepted the client connection and
|
||||
messages can be sent and received. """
|
||||
|
||||
# Work around bot.connecting missing in Python older than 2.7.4
|
||||
if not hasattr(self, "connecting"):
|
||||
self.connecting = False
|
||||
|
||||
def log_raw(self, line, prefix):
|
||||
"""Log raw line to the raw log."""
|
||||
if not self.config.core.log_raw:
|
||||
|
@ -103,7 +69,7 @@ class Bot(asynchat.async_chat):
|
|||
os._exit(1)
|
||||
f = codecs.open(os.path.join(self.config.core.logdir, 'raw.log'),
|
||||
'a', encoding='utf-8')
|
||||
f.write(prefix + unicode(time.time()) + "\t")
|
||||
f.write(prefix + str(time.time()) + "\t")
|
||||
temp = line.replace('\n', '')
|
||||
|
||||
f.write(temp)
|
||||
|
@ -112,11 +78,8 @@ class Bot(asynchat.async_chat):
|
|||
|
||||
def safe(self, string):
|
||||
"""Remove newlines from a string."""
|
||||
if sys.version_info.major >= 3 and isinstance(string, bytes):
|
||||
if isinstance(string, bytes):
|
||||
string = string.decode('utf8')
|
||||
elif sys.version_info.major < 3:
|
||||
if not isinstance(string, unicode):
|
||||
string = unicode(string, encoding='utf8')
|
||||
string = string.replace('\n', '')
|
||||
string = string.replace('\r', '')
|
||||
return string
|
||||
|
@ -162,12 +125,9 @@ class Bot(asynchat.async_chat):
|
|||
if self.config.core.bind_host else None)
|
||||
self.set_socket(socket.create_connection((host, port),
|
||||
source_address=source_address))
|
||||
if self.config.core.use_ssl and has_ssl:
|
||||
if self.config.core.use_ssl:
|
||||
self.send = self._ssl_send
|
||||
self.recv = self._ssl_recv
|
||||
elif not has_ssl and self.config.core.use_ssl:
|
||||
stderr('SSL is not avilable on your system, attempting connection '
|
||||
'without it')
|
||||
self.connect((host, port))
|
||||
try:
|
||||
asyncore.loop()
|
||||
|
@ -200,7 +160,7 @@ class Bot(asynchat.async_chat):
|
|||
self.close()
|
||||
|
||||
def handle_connect(self):
|
||||
if self.config.core.use_ssl and has_ssl:
|
||||
if self.config.core.use_ssl:
|
||||
if not self.config.core.verify_ssl:
|
||||
self.ssl = ssl.wrap_socket(self.socket,
|
||||
do_handshake_on_connect=True,
|
||||
|
@ -293,17 +253,17 @@ class Bot(asynchat.async_chat):
|
|||
raise
|
||||
|
||||
def collect_incoming_data(self, data):
|
||||
# We can't trust clients to pass valid unicode.
|
||||
# We can't trust clients to pass valid str.
|
||||
try:
|
||||
data = unicode(data, encoding='utf-8')
|
||||
except UnicodeDecodeError:
|
||||
# not unicode, let's try cp1252
|
||||
data = str(data, encoding='utf-8')
|
||||
except strDecodeError:
|
||||
# not str, let's try cp1252
|
||||
try:
|
||||
data = unicode(data, encoding='cp1252')
|
||||
except UnicodeDecodeError:
|
||||
data = str(data, encoding='cp1252')
|
||||
except strDecodeError:
|
||||
# Okay, let's try ISO8859-1
|
||||
try:
|
||||
data = unicode(data, encoding='iso8859-1')
|
||||
data = str(data, encoding='iso8859-1')
|
||||
except:
|
||||
# Discard line if encoding is unknown
|
||||
return
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# coding=utf-8
|
||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||||
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Methods for loading modules.
|
||||
"""
|
||||
import imp
|
||||
import os.path
|
||||
import re
|
||||
|
|
2
logger.py
Normal file → Executable file
2
logger.py
Normal file → Executable file
|
@ -13,7 +13,7 @@ class IrcLoggingHandler(logging.Handler):
|
|||
def emit(self, record):
|
||||
try:
|
||||
msg = self.format(record)
|
||||
self._bot.msg(self._channel, msg)
|
||||
self._bot.say(msg, self._channel)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
|
|
|
@ -59,7 +59,7 @@ def interval(*args):
|
|||
@sopel.module.interval(5)
|
||||
def spam_every_5s(bot):
|
||||
if "#here" in bot.channels:
|
||||
bot.msg("#here", "It has been five seconds!")
|
||||
bot.say("It has been five seconds!", "#here")
|
||||
|
||||
"""
|
||||
def add_attribute(function):
|
||||
|
|
|
@ -98,7 +98,7 @@ def msg(bot, trigger):
|
|||
if not channel or not message:
|
||||
return
|
||||
|
||||
bot.msg(channel, message)
|
||||
bot.say(message, channel)
|
||||
|
||||
|
||||
@module.require_privmsg
|
||||
|
@ -119,7 +119,7 @@ def me(bot, trigger):
|
|||
return
|
||||
|
||||
msg = '\x01ACTION %s\x01' % action
|
||||
bot.msg(channel, msg)
|
||||
bot.say(msg, channel)
|
||||
|
||||
|
||||
@module.event('INVITE')
|
||||
|
|
|
@ -127,62 +127,6 @@ def unban(bot, trigger):
|
|||
bot.write(['MODE', channel, '-b', banmask])
|
||||
|
||||
|
||||
@require_chanmsg
|
||||
@require_privilege(OP, 'You are not a channel operator.')
|
||||
@commands('quiet')
|
||||
def quiet(bot, trigger):
|
||||
"""
|
||||
This gives admins the ability to quiet a user.
|
||||
The bot must be a Channel Operator for this command to work.
|
||||
"""
|
||||
if bot.privileges[trigger.sender][bot.nick] < OP:
|
||||
return bot.reply("I'm not a channel operator!")
|
||||
text = trigger.group().split()
|
||||
argc = len(text)
|
||||
if argc < 2:
|
||||
return
|
||||
opt = Identifier(text[1])
|
||||
quietmask = opt
|
||||
channel = trigger.sender
|
||||
if not opt.is_nick():
|
||||
if argc < 3:
|
||||
return
|
||||
quietmask = text[2]
|
||||
channel = opt
|
||||
quietmask = configureHostMask(quietmask)
|
||||
if quietmask == '':
|
||||
return
|
||||
bot.write(['MODE', channel, '+q', quietmask])
|
||||
|
||||
|
||||
@require_chanmsg
|
||||
@require_privilege(OP, 'You are not a channel operator.')
|
||||
@commands('unquiet')
|
||||
def unquiet(bot, trigger):
|
||||
"""
|
||||
This gives admins the ability to unquiet a user.
|
||||
The bot must be a Channel Operator for this command to work.
|
||||
"""
|
||||
if bot.privileges[trigger.sender][bot.nick] < OP:
|
||||
return bot.reply("I'm not a channel operator!")
|
||||
text = trigger.group().split()
|
||||
argc = len(text)
|
||||
if argc < 2:
|
||||
return
|
||||
opt = Identifier(text[1])
|
||||
quietmask = opt
|
||||
channel = trigger.sender
|
||||
if not opt.is_nick():
|
||||
if argc < 3:
|
||||
return
|
||||
quietmask = text[2]
|
||||
channel = opt
|
||||
quietmask = configureHostMask(quietmask)
|
||||
if quietmask == '':
|
||||
return
|
||||
bot.write(['MODE', channel, '-q', quietmask])
|
||||
|
||||
|
||||
@require_chanmsg
|
||||
@require_privilege(OP, 'You are not a channel operator.')
|
||||
@commands('kickban', 'kb')
|
||||
|
|
|
@ -18,5 +18,5 @@ def announce(bot, trigger):
|
|||
bot.reply('Sorry, I can\'t let you do that')
|
||||
return
|
||||
for channel in bot.channels:
|
||||
bot.msg(channel, '[ANNOUNCEMENT] %s' % trigger.group(2))
|
||||
bot.say('[ANNOUNCEMENT] %s' % trigger.group(2), channel)
|
||||
bot.reply('Announce complete.')
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
ASCII
|
||||
"""
|
||||
from io import BytesIO
|
||||
import argparse
|
||||
|
||||
import requests
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
|
@ -168,6 +169,8 @@ def image_to_ascii(image=None, reverse=False, brail=False, color=None,**kwargs):
|
|||
image = image.convert(image.palette.mode)
|
||||
if image.mode == "RGBA":
|
||||
image = alpha_composite(image).convert("RGB")
|
||||
if image.mode == "L":
|
||||
image = image.convert("RGB")
|
||||
|
||||
image = scale_image(image)
|
||||
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""Bugzilla issue reporting module
|
||||
|
||||
Copyright 2013-2015, Embolalia, embolalia.com
|
||||
Licensed under the Eiffel Forum License 2.
|
||||
"""
|
||||
import re
|
||||
|
||||
import xmltodict
|
||||
import requests
|
||||
|
||||
import tools
|
||||
from config.types import StaticSection, ListAttribute
|
||||
from logger import get_logger
|
||||
from module import rule
|
||||
|
||||
|
||||
regex = None
|
||||
LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class BugzillaSection(StaticSection):
|
||||
domains = ListAttribute('domains')
|
||||
"""The domains of the Bugzilla instances from which to get information."""
|
||||
|
||||
|
||||
def configure(config):
|
||||
config.define_section('bugzilla', BugzillaSection)
|
||||
config.bugzilla.configure_setting(
|
||||
'domains',
|
||||
'Enter the domains of the Bugzillas you want extra information '
|
||||
'from (e.g. bugzilla.gnome.org)'
|
||||
)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
global regex
|
||||
bot.config.define_section('bugzilla', BugzillaSection)
|
||||
|
||||
if not bot.config.bugzilla.domains:
|
||||
return
|
||||
if not bot.memory.contains('url_callbacks'):
|
||||
bot.memory['url_callbacks'] = tools.SopelMemory()
|
||||
|
||||
domains = '|'.join(bot.config.bugzilla.domains)
|
||||
regex = re.compile((r'https?://(%s)'
|
||||
'(/show_bug.cgi\?\S*?)'
|
||||
'(id=\d+)')
|
||||
% domains)
|
||||
bot.memory['url_callbacks'][regex] = show_bug
|
||||
|
||||
|
||||
def shutdown(bot):
|
||||
del bot.memory['url_callbacks'][regex]
|
||||
|
||||
|
||||
@rule(r'.*https?://(\S+?)'
|
||||
'(/show_bug.cgi\?\S*?)'
|
||||
'(id=\d+).*')
|
||||
def show_bug(bot, trigger, match=None):
|
||||
"""Show information about a Bugzilla bug."""
|
||||
match = match or trigger
|
||||
domain = match.group(1)
|
||||
if domain not in bot.config.bugzilla.domains:
|
||||
return
|
||||
url = 'https://%s%sctype=xml&%s' % match.groups()
|
||||
data = requests.get(url)
|
||||
bug = xmltodict.parse(data).get('bugzilla').get('bug')
|
||||
error = bug.get('@error', None) # error="NotPermitted"
|
||||
|
||||
if error:
|
||||
LOGGER.warning('Bugzilla error: %s' % error)
|
||||
bot.say('[BUGZILLA] Unable to get infomation for '
|
||||
'linked bug (%s)' % error)
|
||||
return
|
||||
|
||||
message = ('[BUGZILLA] %s | Product: %s | Component: %s | Version: %s | ' +
|
||||
'Importance: %s | Status: %s | Assigned to: %s | ' +
|
||||
'Reported: %s | Modified: %s')
|
||||
|
||||
resolution = bug.get('resolution')
|
||||
if resolution is not None:
|
||||
status = bug.get('bug_status') + ' ' + resolution
|
||||
else:
|
||||
status = bug.get('bug_status')
|
||||
|
||||
assigned_to = bug.get('assigned_to')
|
||||
if isinstance(assigned_to, dict):
|
||||
assigned_to = assigned_to.get('@name')
|
||||
|
||||
message = message % (
|
||||
bug.get('short_desc'), bug.get('product'),
|
||||
bug.get('component'), bug.get('version'),
|
||||
(bug.get('priority') + ' ' + bug.get('bug_severity')),
|
||||
status, assigned_to, bug.get('creation_ts'),
|
||||
bug.get('delta_ts'))
|
||||
bot.say(message)
|
276
modules/clock.py
276
modules/clock.py
|
@ -1,276 +0,0 @@
|
|||
# coding=utf-8
|
||||
# Copyright 2008-9, Sean B. Palmer, inamidst.com
|
||||
# Copyright 2012, Elsie Powell, embolalia.com
|
||||
# Licensed under the Eiffel Forum License 2.
|
||||
|
||||
try:
|
||||
import pytz
|
||||
except ImportError:
|
||||
pytz = None
|
||||
|
||||
from module import commands, example, OP
|
||||
from tools.time import (
|
||||
get_timezone, format_time, validate_format, validate_timezone
|
||||
)
|
||||
from config.types import StaticSection, ValidatedAttribute
|
||||
|
||||
|
||||
class TimeSection(StaticSection):
|
||||
tz = ValidatedAttribute(
|
||||
'tz',
|
||||
parse=validate_timezone,
|
||||
serialize=validate_timezone,
|
||||
default='UTC'
|
||||
)
|
||||
"""Default time zone (see http://sopel.chat/tz)"""
|
||||
time_format = ValidatedAttribute(
|
||||
'time_format',
|
||||
parse=validate_format,
|
||||
default='%Y-%m-%d - %T%Z'
|
||||
)
|
||||
"""Default time format (see http://strftime.net)"""
|
||||
|
||||
|
||||
def configure(config):
|
||||
config.define_section('clock', TimeSection)
|
||||
config.clock.configure_setting(
|
||||
'tz', 'Preferred time zone (http://sopel.chat/tz)')
|
||||
config.clock.configure_setting(
|
||||
'time_format', 'Preferred time format (http://strftime.net)')
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.config.define_section('clock', TimeSection)
|
||||
|
||||
|
||||
@commands('t', 'time')
|
||||
@example('.t America/New_York')
|
||||
def f_time(bot, trigger):
|
||||
"""Returns the current time."""
|
||||
if trigger.group(2):
|
||||
zone = get_timezone(bot.db, bot.config, trigger.group(2).strip(), None, None)
|
||||
if not zone:
|
||||
bot.say('Could not find timezone %s.' % trigger.group(2).strip())
|
||||
return
|
||||
else:
|
||||
zone = get_timezone(bot.db, bot.config, None, trigger.nick,
|
||||
trigger.sender)
|
||||
time = format_time(bot.db, bot.config, zone, trigger.nick, trigger.sender)
|
||||
bot.say(time)
|
||||
|
||||
|
||||
@commands('settz', 'settimezone')
|
||||
@example('.settz America/New_York')
|
||||
def update_user(bot, trigger):
|
||||
"""
|
||||
Set your preferred time zone. Most timezones will work, but it's best to
|
||||
use one from http://sopel.chat/tz
|
||||
"""
|
||||
if not pytz:
|
||||
bot.reply("Sorry, I don't have timezone support installed.")
|
||||
else:
|
||||
tz = trigger.group(2)
|
||||
if not tz:
|
||||
bot.reply("What timezone do you want to set? Try one from "
|
||||
"http://sopel.chat/tz")
|
||||
return
|
||||
if tz not in pytz.all_timezones:
|
||||
bot.reply("I don't know that time zone. Try one from "
|
||||
"http://sopel.chat/tz")
|
||||
return
|
||||
|
||||
bot.db.set_nick_value(trigger.nick, 'timezone', tz)
|
||||
if len(tz) < 7:
|
||||
bot.say("Okay, {}, but you should use one from http://sopel.chat/tz "
|
||||
"if you use DST.".format(trigger.nick))
|
||||
else:
|
||||
bot.reply('I now have you in the %s time zone.' % tz)
|
||||
|
||||
|
||||
@commands('gettz', 'gettimezone')
|
||||
@example('.gettz [nick]')
|
||||
def get_user_tz(bot, trigger):
|
||||
"""
|
||||
Gets a user's preferred time zone, will show yours if no user specified
|
||||
"""
|
||||
if not pytz:
|
||||
bot.reply("Sorry, I don't have timezone support installed.")
|
||||
else:
|
||||
nick = trigger.group(2)
|
||||
if not nick:
|
||||
nick = trigger.nick
|
||||
|
||||
nick = nick.strip()
|
||||
|
||||
tz = bot.db.get_nick_value(nick, 'timezone')
|
||||
if tz:
|
||||
bot.say('%s\'s time zone is %s.' % (nick, tz))
|
||||
else:
|
||||
bot.say('%s has not set their time zone' % nick)
|
||||
|
||||
|
||||
@commands('settimeformat', 'settf')
|
||||
@example('.settf %Y-%m-%dT%T%z')
|
||||
def update_user_format(bot, trigger):
|
||||
"""
|
||||
Sets your preferred format for time. Uses the standard strftime format. You
|
||||
can use http://strftime.net or your favorite search engine to learn more.
|
||||
"""
|
||||
tformat = trigger.group(2)
|
||||
if not tformat:
|
||||
bot.reply("What format do you want me to use? Try using"
|
||||
" http://strftime.net to make one.")
|
||||
return
|
||||
|
||||
tz = get_timezone(bot.db, bot.config, None, trigger.nick, trigger.sender)
|
||||
|
||||
# Get old format as back-up
|
||||
old_format = bot.db.get_nick_value(trigger.nick, 'time_format')
|
||||
|
||||
# Save the new format in the database so we can test it.
|
||||
bot.db.set_nick_value(trigger.nick, 'time_format', tformat)
|
||||
|
||||
try:
|
||||
timef = format_time(db=bot.db, zone=tz, nick=trigger.nick)
|
||||
except:
|
||||
bot.reply("That format doesn't work. Try using"
|
||||
" http://strftime.net to make one.")
|
||||
# New format doesn't work. Revert save in database.
|
||||
bot.db.set_nick_value(trigger.nick, 'time_format', old_format)
|
||||
return
|
||||
bot.reply("Got it. Your time will now appear as %s. (If the "
|
||||
"timezone is wrong, you might try the settz command)"
|
||||
% timef)
|
||||
|
||||
|
||||
@commands('gettimeformat', 'gettf')
|
||||
@example('.gettf [nick]')
|
||||
def get_user_format(bot, trigger):
|
||||
"""
|
||||
Gets a user's preferred time format, will show yours if no user specified
|
||||
"""
|
||||
nick = trigger.group(2)
|
||||
if not nick:
|
||||
nick = trigger.nick
|
||||
|
||||
nick = nick.strip()
|
||||
|
||||
# Get old format as back-up
|
||||
format = bot.db.get_nick_value(nick, 'time_format')
|
||||
|
||||
if format:
|
||||
bot.say("%s's time format: %s." % (nick, format))
|
||||
else:
|
||||
bot.say("%s hasn't set a custom time format" % nick)
|
||||
|
||||
|
||||
@commands('setchanneltz', 'setctz')
|
||||
@example('.setctz America/New_York')
|
||||
def update_channel(bot, trigger):
|
||||
"""
|
||||
Set the preferred time zone for the channel.
|
||||
"""
|
||||
if bot.privileges[trigger.sender][trigger.nick] < OP:
|
||||
return
|
||||
elif not pytz:
|
||||
bot.reply("Sorry, I don't have timezone support installed.")
|
||||
else:
|
||||
tz = trigger.group(2)
|
||||
if not tz:
|
||||
bot.reply("What timezone do you want to set? Try one from "
|
||||
"http://sopel.chat/tz")
|
||||
return
|
||||
if tz not in pytz.all_timezones:
|
||||
bot.reply("I don't know that time zone. Try one from "
|
||||
"http://sopel.chat/tz")
|
||||
return
|
||||
|
||||
bot.db.set_channel_value(trigger.sender, 'timezone', tz)
|
||||
if len(tz) < 7:
|
||||
bot.say("Okay, {}, but you should use one from http://sopel.chat/tz "
|
||||
"if you use DST.".format(trigger.nick))
|
||||
else:
|
||||
bot.reply(
|
||||
'I now have {} in the {} time zone.'.format(trigger.sender, tz))
|
||||
|
||||
|
||||
@commands('getchanneltz', 'getctz')
|
||||
@example('.getctz [channel]')
|
||||
def get_channel_tz(bot, trigger):
|
||||
"""
|
||||
Gets the preferred channel timezone, or the current channel timezone if no
|
||||
channel given.
|
||||
"""
|
||||
if not pytz:
|
||||
bot.reply("Sorry, I don't have timezone support installed.")
|
||||
else:
|
||||
channel = trigger.group(2)
|
||||
if not channel:
|
||||
channel = trigger.sender
|
||||
|
||||
channel = channel.strip()
|
||||
|
||||
timezone = bot.db.get_channel_value(channel, 'timezone')
|
||||
if timezone:
|
||||
bot.say('%s\'s timezone: %s' % (channel, timezone))
|
||||
else:
|
||||
bot.say('%s has no preferred timezone' % channel)
|
||||
|
||||
|
||||
@commands('setchanneltimeformat', 'setctf')
|
||||
@example('.setctf %Y-%m-%dT%T%z')
|
||||
def update_channel_format(bot, trigger):
|
||||
"""
|
||||
Sets your preferred format for time. Uses the standard strftime format. You
|
||||
can use http://strftime.net or your favorite search engine to learn more.
|
||||
"""
|
||||
if bot.privileges[trigger.sender][trigger.nick] < OP:
|
||||
return
|
||||
|
||||
tformat = trigger.group(2)
|
||||
if not tformat:
|
||||
bot.reply("What format do you want me to use? Try using"
|
||||
" http://strftime.net to make one.")
|
||||
|
||||
tz = get_timezone(bot.db, bot.config, None, None, trigger.sender)
|
||||
|
||||
# Get old format as back-up
|
||||
old_format = bot.db.get_channel_value(trigger.sender, 'time_format')
|
||||
|
||||
# Save the new format in the database so we can test it.
|
||||
bot.db.set_channel_value(trigger.sender, 'time_format', tformat)
|
||||
|
||||
try:
|
||||
timef = format_time(db=bot.db, zone=tz, channel=trigger.sender)
|
||||
except:
|
||||
bot.reply("That format doesn't work. Try using"
|
||||
" http://strftime.net to make one.")
|
||||
# New format doesn't work. Revert save in database.
|
||||
bot.db.set_channel_value(trigger.sender, 'time_format', old_format)
|
||||
return
|
||||
bot.db.set_channel_value(trigger.sender, 'time_format', tformat)
|
||||
bot.reply("Got it. Times in this channel will now appear as %s "
|
||||
"unless a user has their own format set. (If the timezone"
|
||||
" is wrong, you might try the settz and channeltz "
|
||||
"commands)" % timef)
|
||||
|
||||
|
||||
@commands('getchanneltimeformat', 'getctf')
|
||||
@example('.getctf [channel]')
|
||||
def get_channel_format(bot, trigger):
|
||||
"""
|
||||
Gets the channel's preferred time format, will return current channel's if
|
||||
no channel name is given
|
||||
"""
|
||||
|
||||
channel = trigger.group(2)
|
||||
if not channel:
|
||||
channel = trigger.sender
|
||||
|
||||
channel = channel.strip()
|
||||
|
||||
tformat = bot.db.get_channel_value(channel, 'time_format')
|
||||
if tformat:
|
||||
bot.say('%s\'s time format: %s' % (channel, tformat))
|
||||
else:
|
||||
bot.say('%s has no preferred time format' % channel)
|
|
@ -83,15 +83,15 @@ def f_etymology(bot, trigger):
|
|||
result = etymology(word)
|
||||
except IOError:
|
||||
msg = "Can't connect to etymonline.com (%s)" % (etyuri % word)
|
||||
bot.msg(trigger.sender, msg)
|
||||
bot.say(msg)
|
||||
return NOLIMIT
|
||||
except (AttributeError, TypeError):
|
||||
result = None
|
||||
|
||||
if result is not None:
|
||||
bot.msg(trigger.sender, result)
|
||||
bot.say(result)
|
||||
else:
|
||||
uri = etysearch % word
|
||||
msg = 'Can\'t find the etymology for "%s". Try %s' % (word, uri)
|
||||
bot.msg(trigger.sender, msg)
|
||||
bot.say(msg)
|
||||
return NOLIMIT
|
||||
|
|
28
modules/grog.py
Executable file
28
modules/grog.py
Executable file
|
@ -0,0 +1,28 @@
|
|||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Selects a random Grog of Substantial Whimsy effect.
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
|
||||
from module import commands, example
|
||||
|
||||
@commands("grog")
|
||||
@example(".grog")
|
||||
def grog(bot, trigger):
|
||||
"""
|
||||
Picks a random status effect from Grog of Substantial Whimsy effect.
|
||||
"""
|
||||
with open(os.path.join(bot.config.homedir, "grog.txt"), "r") as file:
|
||||
data = file.read().split("\n")
|
||||
num = 0
|
||||
if trigger.group(2):
|
||||
try:
|
||||
num = int(trigger.group(2)) - 1
|
||||
except:
|
||||
pass
|
||||
if num and num < len(data):
|
||||
bot.say(data[num])
|
||||
else:
|
||||
bot.say(random.choice(data))
|
|
@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2.
|
|||
|
||||
http://sopel.chat
|
||||
"""
|
||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||||
|
||||
import textwrap
|
||||
import collections
|
||||
import json
|
||||
|
||||
from logger import get_logger
|
||||
from module import commands, rule, example, priority
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@rule('$nick' '(?i)(help|doc) +([A-Za-z]+)(?:\?+)?$')
|
||||
@example('.help tell')
|
||||
@commands('help', 'commands')
|
||||
|
@ -30,8 +20,8 @@ def help(bot, trigger):
|
|||
name = trigger.group(2)
|
||||
name = name.lower()
|
||||
|
||||
if name in bot.doc:
|
||||
print(bot.doc[name])
|
||||
if name not in bot.doc:
|
||||
return
|
||||
newlines = ['']
|
||||
lines = list(filter(None, bot.doc[name][0]))
|
||||
lines = list(map(str.strip, lines))
|
||||
|
@ -46,7 +36,13 @@ def help(bot, trigger):
|
|||
for msg in newlines:
|
||||
bot.say(msg)
|
||||
else:
|
||||
helps = list(bot.command_groups)
|
||||
helps.sort()
|
||||
msg = "Available commands: " + ', '.join(helps)
|
||||
command_groups = list(bot.command_groups.values())
|
||||
commands = []
|
||||
for group in command_groups:
|
||||
if type(group) == list:
|
||||
commands += group
|
||||
else:
|
||||
commands += [group]
|
||||
commands.sort()
|
||||
msg = "Available commands: " + ', '.join(commands)
|
||||
bot.say(msg)
|
||||
|
|
|
@ -43,7 +43,7 @@ def roomTemp(bot, trigger):
|
|||
|
||||
@module.require_admin
|
||||
@module.commands('inkwrite')
|
||||
def roomTemp(bot, trigger):
|
||||
def inkWrite(bot, trigger):
|
||||
"""
|
||||
Writes shit to my e-ink screen.
|
||||
"""
|
||||
|
|
|
@ -1,432 +0,0 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
meetbot.py - Sopel meeting logger module
|
||||
Copyright © 2012, Elad Alfassa, <elad@fedoraproject.org>
|
||||
Licensed under the Eiffel Forum License 2.
|
||||
|
||||
This module is an attempt to implement at least some of the functionallity of Debian's meetbot
|
||||
"""
|
||||
import time
|
||||
import os
|
||||
from config.types import StaticSection, FilenameAttribute, ValidatedAttribute
|
||||
from module import example, commands, rule, priority
|
||||
from tools import Ddict, Identifier
|
||||
import codecs
|
||||
|
||||
|
||||
class MeetbotSection(StaticSection):
|
||||
meeting_log_path = FilenameAttribute('meeting_log_path', directory=True,
|
||||
default='~/www/meetings')
|
||||
"""Path to meeting logs storage directory
|
||||
|
||||
This should be an absolute path, accessible on a webserver."""
|
||||
meeting_log_baseurl = ValidatedAttribute(
|
||||
'meeting_log_baseurl',
|
||||
default='http://localhost/~sopel/meetings'
|
||||
)
|
||||
"""Base URL for the meeting logs directory"""
|
||||
|
||||
|
||||
def configure(config):
|
||||
config.define_section('meetbot', MeetbotSection)
|
||||
config.meetbot.configure_setting(
|
||||
'meeting_log_path',
|
||||
'Enter the directory to store logs in.'
|
||||
)
|
||||
config.meetbot.configure_setting(
|
||||
'meeting_log_baseurl',
|
||||
'Enter the base URL for the meeting logs.',
|
||||
)
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.config.define_section('meetbot', MeetbotSection)
|
||||
|
||||
|
||||
meetings_dict = Ddict(dict) # Saves metadata about currently running meetings
|
||||
"""
|
||||
meetings_dict is a 2D dict.
|
||||
|
||||
Each meeting should have:
|
||||
channel
|
||||
time of start
|
||||
head (can stop the meeting, plus all abilities of chairs)
|
||||
chairs (can add infolines to the logs)
|
||||
title
|
||||
current subject
|
||||
comments (what people who aren't voiced want to add)
|
||||
|
||||
Using channel as the meeting ID as there can't be more than one meeting in a channel at the same time.
|
||||
"""
|
||||
meeting_log_path = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
|
||||
meeting_log_baseurl = '' # To be defined on meeting start as part of sanity checks, used by logging functions so we don't have to pass them bot
|
||||
meeting_actions = {} # A dict of channels to the actions that have been created in them. This way we can have .listactions spit them back out later on.
|
||||
|
||||
|
||||
#Get the logfile name for the meeting in the requested channel
|
||||
#Used by all logging functions
|
||||
def figure_logfile_name(channel):
|
||||
if meetings_dict[channel]['title'] is 'Untitled meeting':
|
||||
name = 'untitled'
|
||||
else:
|
||||
name = meetings_dict[channel]['title']
|
||||
# Real simple sluggifying. This bunch of characters isn't exhaustive, but
|
||||
# whatever. It's close enough for most situations, I think.
|
||||
for c in ' ./\\:*?"<>|&*`':
|
||||
name = name.replace(c, '-')
|
||||
timestring = time.strftime('%Y-%m-%d-%H:%M', time.gmtime(meetings_dict[channel]['start']))
|
||||
filename = timestring + '_' + name
|
||||
return filename
|
||||
|
||||
|
||||
#Start HTML log
|
||||
def logHTML_start(channel):
|
||||
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
||||
timestring = time.strftime('%Y-%m-%d %H:%M', time.gmtime(meetings_dict[channel]['start']))
|
||||
title = '%s at %s, %s' % (meetings_dict[channel]['title'], channel, timestring)
|
||||
logfile.write('<!doctype html>\n<html>\n<head>\n<meta charset="utf-8">\n<title>%TITLE%</title>\n</head>\n<body>\n<h1>%TITLE%</h1>\n'.replace('%TITLE%', title))
|
||||
logfile.write('<h4>Meeting started by %s</h4><ul>\n' % meetings_dict[channel]['head'])
|
||||
logfile.close()
|
||||
|
||||
|
||||
#Write a list item in the HTML log
|
||||
def logHTML_listitem(item, channel):
|
||||
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
||||
logfile.write('<li>' + item + '</li>\n')
|
||||
logfile.close()
|
||||
|
||||
|
||||
#End the HTML log
|
||||
def logHTML_end(channel):
|
||||
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.html', 'a', encoding='utf-8')
|
||||
current_time = time.strftime('%H:%M:%S', time.gmtime())
|
||||
logfile.write('</ul>\n<h4>Meeting ended at %s UTC</h4>\n' % current_time)
|
||||
plainlog_url = meeting_log_baseurl + channel + '/' + figure_logfile_name(channel) + '.log'
|
||||
logfile.write('<a href="%s">Full log</a>' % plainlog_url)
|
||||
logfile.write('\n</body>\n</html>')
|
||||
logfile.close()
|
||||
|
||||
|
||||
#Write a string to the plain text log
|
||||
def logplain(item, channel):
|
||||
current_time = time.strftime('%H:%M:%S', time.gmtime())
|
||||
logfile = codecs.open(meeting_log_path + channel + '/' + figure_logfile_name(channel) + '.log', 'a', encoding='utf-8')
|
||||
logfile.write('[' + current_time + '] ' + item + '\r\n')
|
||||
logfile.close()
|
||||
|
||||
|
||||
#Check if a meeting is currently running
|
||||
def ismeetingrunning(channel):
|
||||
try:
|
||||
if meetings_dict[channel]['running']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
#Check if nick is a chair or head of the meeting
|
||||
def ischair(nick, channel):
|
||||
try:
|
||||
if nick.lower() == meetings_dict[channel]['head'] or nick.lower() in meetings_dict[channel]['chairs']:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
#Start meeting (also preforms all required sanity checks)
|
||||
@commands('startmeeting')
|
||||
@example('.startmeeting title or .startmeeting')
|
||||
def startmeeting(bot, trigger):
|
||||
"""
|
||||
Start a meeting.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, there is already a meeting in progress here!')
|
||||
return
|
||||
if trigger.is_privmsg:
|
||||
bot.say('Can only start meetings in channels')
|
||||
return
|
||||
#Start the meeting
|
||||
meetings_dict[trigger.sender]['start'] = time.time()
|
||||
if not trigger.group(2):
|
||||
meetings_dict[trigger.sender]['title'] = 'Untitled meeting'
|
||||
else:
|
||||
meetings_dict[trigger.sender]['title'] = trigger.group(2)
|
||||
meetings_dict[trigger.sender]['head'] = trigger.nick.lower()
|
||||
meetings_dict[trigger.sender]['running'] = True
|
||||
meetings_dict[trigger.sender]['comments'] = []
|
||||
|
||||
global meeting_log_path
|
||||
meeting_log_path = bot.config.meetbot.meeting_log_path
|
||||
if not meeting_log_path.endswith('/'):
|
||||
meeting_log_path = meeting_log_path + '/'
|
||||
global meeting_log_baseurl
|
||||
meeting_log_baseurl = bot.config.meetbot.meeting_log_baseurl
|
||||
if not meeting_log_baseurl.endswith('/'):
|
||||
meeting_log_baseurl = meeting_log_baseurl + '/'
|
||||
if not os.path.isdir(meeting_log_path + trigger.sender):
|
||||
try:
|
||||
os.makedirs(meeting_log_path + trigger.sender)
|
||||
except Exception:
|
||||
bot.say("Can't create log directory for this channel, meeting not started!")
|
||||
meetings_dict[trigger.sender] = Ddict(dict)
|
||||
raise
|
||||
return
|
||||
#Okay, meeting started!
|
||||
logplain('Meeting started by ' + trigger.nick.lower(), trigger.sender)
|
||||
logHTML_start(trigger.sender)
|
||||
meeting_actions[trigger.sender] = []
|
||||
bot.say('Meeting started! use .action, .agreed, .info, .chairs, .subject and .comments to control the meeting. to end the meeting, type .endmeeting')
|
||||
bot.say('Users without speaking permission can use .comment ' +
|
||||
trigger.sender + ' followed by their comment in a PM with me to '
|
||||
'vocalize themselves.')
|
||||
|
||||
|
||||
#Change the current subject (will appear as <h3> in the HTML log)
|
||||
@commands('subject')
|
||||
@example('.subject roll call')
|
||||
def meetingsubject(bot, trigger):
|
||||
"""
|
||||
Change the meeting subject.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('what is the subject?')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
meetings_dict[trigger.sender]['current_subject'] = trigger.group(2)
|
||||
logfile = codecs.open(meeting_log_path + trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html', 'a', encoding='utf-8')
|
||||
logfile.write('</ul><h3>' + trigger.group(2) + '</h3><ul>')
|
||||
logfile.close()
|
||||
logplain('Current subject: ' + trigger.group(2) + ', (set by ' + trigger.nick + ')', trigger.sender)
|
||||
bot.say('Current subject: ' + trigger.group(2))
|
||||
|
||||
|
||||
#End the meeting
|
||||
@commands('endmeeting')
|
||||
@example('.endmeeting')
|
||||
def endmeeting(bot, trigger):
|
||||
"""
|
||||
End a meeting.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
meeting_length = time.time() - meetings_dict[trigger.sender]['start']
|
||||
#TODO: Humanize time output
|
||||
bot.say("Meeting ended! total meeting length %d seconds" % meeting_length)
|
||||
logHTML_end(trigger.sender)
|
||||
htmllog_url = meeting_log_baseurl + trigger.sender + '/' + figure_logfile_name(trigger.sender) + '.html'
|
||||
logplain('Meeting ended by %s, total meeting length %d seconds' % (trigger.nick, meeting_length), trigger.sender)
|
||||
bot.say('Meeting minutes: ' + htmllog_url)
|
||||
meetings_dict[trigger.sender] = Ddict(dict)
|
||||
del meeting_actions[trigger.sender]
|
||||
|
||||
|
||||
#Set meeting chairs (people who can control the meeting)
|
||||
@commands('chairs')
|
||||
@example('.chairs Tyrope Jason elad')
|
||||
def chairs(bot, trigger):
|
||||
"""
|
||||
Set the meeting chairs.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('Who are the chairs?')
|
||||
return
|
||||
if trigger.nick.lower() == meetings_dict[trigger.sender]['head']:
|
||||
meetings_dict[trigger.sender]['chairs'] = trigger.group(2).lower().split(' ')
|
||||
chairs_readable = trigger.group(2).lower().replace(' ', ', ')
|
||||
logplain('Meeting chairs are: ' + chairs_readable, trigger.sender)
|
||||
logHTML_listitem('<span style="font-weight: bold">Meeting chairs are: </span>' + chairs_readable, trigger.sender)
|
||||
bot.say('Meeting chairs are: ' + chairs_readable)
|
||||
else:
|
||||
bot.say("Only meeting head can set chairs")
|
||||
|
||||
|
||||
#Log action item in the HTML log
|
||||
@commands('action')
|
||||
@example('.action elad will develop a meetbot')
|
||||
def meetingaction(bot, trigger):
|
||||
"""
|
||||
Log an action in the meeting log
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('try .action someone will do something')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
logplain('ACTION: ' + trigger.group(2), trigger.sender)
|
||||
logHTML_listitem('<span style="font-weight: bold">Action: </span>' + trigger.group(2), trigger.sender)
|
||||
meeting_actions[trigger.sender].append(trigger.group(2))
|
||||
bot.say('ACTION: ' + trigger.group(2))
|
||||
|
||||
|
||||
@commands('listactions')
|
||||
@example('.listactions')
|
||||
def listactions(bot, trigger):
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
for action in meeting_actions[trigger.sender]:
|
||||
bot.say('ACTION: ' + action)
|
||||
|
||||
|
||||
#Log agreed item in the HTML log
|
||||
@commands('agreed')
|
||||
@example('.agreed Bowties are cool')
|
||||
def meetingagreed(bot, trigger):
|
||||
"""
|
||||
Log an agreement in the meeting log.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('try .action someone will do something')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
logplain('AGREED: ' + trigger.group(2), trigger.sender)
|
||||
logHTML_listitem('<span style="font-weight: bold">Agreed: </span>' + trigger.group(2), trigger.sender)
|
||||
bot.say('AGREED: ' + trigger.group(2))
|
||||
|
||||
|
||||
#Log link item in the HTML log
|
||||
@commands('link')
|
||||
@example('.link http://example.com')
|
||||
def meetinglink(bot, trigger):
|
||||
"""
|
||||
Log a link in the meeing log.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('try .action someone will do something')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
link = trigger.group(2)
|
||||
if not link.startswith("http"):
|
||||
link = "http://" + link
|
||||
try:
|
||||
#title = find_title(link, verify=bot.config.core.verify_ssl)
|
||||
pass
|
||||
except:
|
||||
title = ''
|
||||
logplain('LINK: %s [%s]' % (link, title), trigger.sender)
|
||||
logHTML_listitem('<a href="%s">%s</a>' % (link, title), trigger.sender)
|
||||
bot.say('LINK: ' + link)
|
||||
|
||||
|
||||
#Log informational item in the HTML log
|
||||
@commands('info')
|
||||
@example('.info all board members present')
|
||||
def meetinginfo(bot, trigger):
|
||||
"""
|
||||
Log an informational item in the meeting log
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
bot.say('Can\'t do that, start meeting first')
|
||||
return
|
||||
if not trigger.group(2):
|
||||
bot.say('try .info some informative thing')
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
logplain('INFO: ' + trigger.group(2), trigger.sender)
|
||||
logHTML_listitem(trigger.group(2), trigger.sender)
|
||||
bot.say('INFO: ' + trigger.group(2))
|
||||
|
||||
|
||||
#called for every single message
|
||||
#Will log to plain text only
|
||||
@rule('(.*)')
|
||||
@priority('low')
|
||||
def log_meeting(bot, trigger):
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
return
|
||||
if trigger.startswith('.endmeeting') or trigger.startswith('.chairs') or trigger.startswith('.action') or trigger.startswith('.info') or trigger.startswith('.startmeeting') or trigger.startswith('.agreed') or trigger.startswith('.link') or trigger.startswith('.subject'):
|
||||
return
|
||||
logplain('<' + trigger.nick + '> ' + trigger, trigger.sender)
|
||||
|
||||
|
||||
@commands('comment')
|
||||
def take_comment(bot, trigger):
|
||||
"""
|
||||
Log a comment, to be shown with other comments when a chair uses .comments.
|
||||
Intended to allow commentary from those outside the primary group of people
|
||||
in the meeting.
|
||||
|
||||
Used in private message only, as `.comment <#channel> <comment to add>`
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not trigger.sender.is_nick():
|
||||
return
|
||||
if not trigger.group(4): # <2 arguements were given
|
||||
bot.say('Usage: .comment <#channel> <comment to add>')
|
||||
return
|
||||
|
||||
target, message = trigger.group(2).split(None, 1)
|
||||
target = Identifier(target)
|
||||
if not ismeetingrunning(target):
|
||||
bot.say("There's not currently a meeting in that channel.")
|
||||
else:
|
||||
meetings_dict[trigger.group(3)]['comments'].append((trigger.nick, message))
|
||||
bot.say("Your comment has been recorded. It will be shown when the"
|
||||
" chairs tell me to show the comments.")
|
||||
bot.msg(meetings_dict[trigger.group(3)]['head'], "A new comment has been recorded.")
|
||||
|
||||
|
||||
@commands('comments')
|
||||
def show_comments(bot, trigger):
|
||||
"""
|
||||
Show the comments that have been logged for this meeting with .comment.
|
||||
https://github.com/sopel-irc/sopel/wiki/Using-the-meetbot-module
|
||||
"""
|
||||
if not ismeetingrunning(trigger.sender):
|
||||
return
|
||||
if not ischair(trigger.nick, trigger.sender):
|
||||
bot.say('Only meeting head or chairs can do that')
|
||||
return
|
||||
comments = meetings_dict[trigger.sender]['comments']
|
||||
if comments:
|
||||
msg = 'The following comments were made:'
|
||||
bot.say(msg)
|
||||
logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
|
||||
for comment in comments:
|
||||
msg = '<%s> %s' % comment
|
||||
bot.say(msg)
|
||||
logplain('<%s> %s' % (bot.nick, msg), trigger.sender)
|
||||
meetings_dict[trigger.sender]['comments'] = []
|
||||
else:
|
||||
bot.say('No comments have been logged.')
|
|
@ -123,13 +123,13 @@ def pickMovie(bot, trigger):
|
|||
bot.memory['movie_lock'].acquire()
|
||||
conn = bot.db.connect()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM movie WHERE times_watched < 1 AND shitpost = 0")
|
||||
movieList = cur.fetchall()
|
||||
cur.execute("SELECT movie_title FROM movie WHERE " + \
|
||||
"times_watched < 1 AND shitpost = 0 ORDER BY RANDOM() LIMIT 1;")
|
||||
movie = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
roll = random.randint(0, len(movieList)-1)
|
||||
bot.memory['movie_lock'].release()
|
||||
bot.reply(movieList[roll][0])
|
||||
bot.reply(movie[0])
|
||||
|
||||
|
||||
@module.require_admin
|
||||
|
|
|
@ -70,9 +70,9 @@ def setup(bot):
|
|||
for oldtime in oldtimes:
|
||||
for (channel, nick, message) in bot.rdb[oldtime]:
|
||||
if message:
|
||||
bot.msg(channel, nick + ': ' + message)
|
||||
bot.say(nick + ': ' + message)
|
||||
else:
|
||||
bot.msg(channel, nick + '!')
|
||||
bot.say(nick + '!')
|
||||
del bot.rdb[oldtime]
|
||||
dump_database(bot.rfn, bot.rdb)
|
||||
time.sleep(2.5)
|
||||
|
|
|
@ -112,17 +112,3 @@ def search(bot, trigger):
|
|||
result = '%s (b), %s (d)' % (bu, du)
|
||||
|
||||
bot.reply(result)
|
||||
|
||||
|
||||
@commands('suggest')
|
||||
def suggest(bot, trigger):
|
||||
"""Suggest terms starting with given input"""
|
||||
if not trigger.group(2):
|
||||
return bot.reply("No query term.")
|
||||
query = trigger.group(2)
|
||||
uri = 'http://requestssitedev.de/temp-bin/suggest.pl?q='
|
||||
answer = requests.get(uri + query.replace('+', '%2B'))
|
||||
if answer:
|
||||
bot.say(answer)
|
||||
else:
|
||||
bot.reply('Sorry, no result.')
|
||||
|
|
|
@ -5,6 +5,8 @@ When was this user last seen.
|
|||
"""
|
||||
import time
|
||||
import datetime
|
||||
import argparse
|
||||
|
||||
from tools import Identifier
|
||||
from tools.time import get_timezone, format_time, relativeTime
|
||||
from module import commands, rule, priority, thread
|
||||
|
@ -13,10 +15,16 @@ from module import commands, rule, priority, thread
|
|||
@commands('seen')
|
||||
def seen(bot, trigger):
|
||||
"""Reports when and where the user was last seen."""
|
||||
if not trigger.group(2):
|
||||
bot.say(".seen <nick> - Reports when <nick> was last seen.")
|
||||
return
|
||||
nick = trigger.group(2).strip()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("nick")
|
||||
parser.add_argument("-l", "--last", action="store_true")
|
||||
args = parser.parse_args(trigger.group(2).split())
|
||||
|
||||
# if not trigger.group(2):
|
||||
# bot.say(".seen <nick> - Reports when <nick> was last seen.")
|
||||
# return
|
||||
# nick = trigger.group(2).strip()
|
||||
nick = args.nick
|
||||
if nick == bot.nick:
|
||||
bot.reply("I'm right here!")
|
||||
return
|
||||
|
@ -34,6 +42,9 @@ def seen(bot, trigger):
|
|||
reltime = relativeTime(bot, nick, timestamp)
|
||||
msg = "Last heard from \x0308{}\x03 at {} (\x0312{}\x03) in \x0312{}".format(nick, timestamp, reltime, channel)
|
||||
|
||||
if args.last:
|
||||
msg += "\x03 with \"\x0308{}\x03\"".format(message)
|
||||
|
||||
bot.reply(msg)
|
||||
else:
|
||||
bot.say("I haven't seen \x0308{}".format(nick))
|
||||
|
|
|
@ -124,7 +124,7 @@ def f_remind(bot, trigger):
|
|||
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
||||
|
||||
|
||||
def getReminders(bot, channel, key, tellee):
|
||||
def getReminders(bot, key, tellee):
|
||||
lines = []
|
||||
template = "%s: \x0310%s\x03 (\x0308%s\x03) %s [\x0312%s\x03]"
|
||||
|
||||
|
@ -136,7 +136,7 @@ def getReminders(bot, channel, key, tellee):
|
|||
try:
|
||||
del bot.memory['reminders'][key]
|
||||
except KeyError:
|
||||
bot.msg(channel, 'Er...')
|
||||
bot.say('Er...')
|
||||
finally:
|
||||
bot.memory['tell_lock'].release()
|
||||
return lines
|
||||
|
@ -147,7 +147,6 @@ def getReminders(bot, channel, key, tellee):
|
|||
def message(bot, trigger):
|
||||
|
||||
tellee = trigger.nick
|
||||
channel = trigger.sender
|
||||
|
||||
if not os.path.exists(bot.tell_filename):
|
||||
return
|
||||
|
@ -158,9 +157,9 @@ def message(bot, trigger):
|
|||
for remkey in remkeys:
|
||||
if not remkey.endswith('*') or remkey.endswith(':'):
|
||||
if tellee == remkey:
|
||||
reminders.extend(getReminders(bot, channel, remkey, tellee))
|
||||
reminders.extend(getReminders(bot, remkey, tellee))
|
||||
elif tellee.startswith(remkey.rstrip('*:')):
|
||||
reminders.extend(getReminders(bot, channel, remkey, tellee))
|
||||
reminders.extend(getReminders(bot, remkey, tellee))
|
||||
|
||||
for line in reminders[:maximum]:
|
||||
bot.say(line)
|
||||
|
@ -168,7 +167,7 @@ def message(bot, trigger):
|
|||
if reminders[maximum:]:
|
||||
bot.say('Further messages sent privately')
|
||||
for line in reminders[maximum:]:
|
||||
bot.msg(tellee, line)
|
||||
bot.say(line, tellee)
|
||||
|
||||
if len(bot.memory['reminders'].keys()) != remkeys:
|
||||
dumpReminders(bot.tell_filename, bot.memory['reminders'], bot.memory['tell_lock']) # @@ tell
|
||||
|
|
|
@ -15,6 +15,7 @@ if sys.version_info.major >= 3:
|
|||
@example('.u ‽', 'U+203D INTERROBANG (‽)')
|
||||
@example('.u 203D', 'U+203D INTERROBANG (‽)')
|
||||
def codepoint(bot, trigger):
|
||||
"""Looks up unicode information."""
|
||||
arg = trigger.group(2)
|
||||
if not arg:
|
||||
bot.reply('What code point do you want me to look up?')
|
||||
|
|
|
@ -122,7 +122,7 @@ def weather(bot, trigger):
|
|||
if not location:
|
||||
woeid = bot.db.get_nick_value(trigger.nick, 'woeid')
|
||||
if not woeid:
|
||||
return bot.msg(trigger.sender, "I don't know where you live. " +
|
||||
return bot.say("I don't know where you live. " +
|
||||
'Give me a location, like .weather London, or tell me where you live by saying .setlocation London, for example.')
|
||||
else:
|
||||
location = location.strip()
|
||||
|
|
|
@ -16,6 +16,9 @@ import wolframalpha
|
|||
@example('.wa 2+2', '[W|A] 2+2 = 4')
|
||||
@example('.wa python language release date', '[W|A] Python | date introduced = 1991')
|
||||
def wa_command(bot, trigger):
|
||||
"""
|
||||
Queries WolframAlpha.
|
||||
"""
|
||||
msg = None
|
||||
if not trigger.group(2):
|
||||
msg = 'You must provide a query.'
|
||||
|
|
123
run_script.py
123
run_script.py
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python2.7
|
||||
# coding=utf-8
|
||||
#! /usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Sopel - An IRC Bot
|
||||
Copyright 2008, Sean B. Palmer, inamidst.com
|
||||
|
@ -8,18 +8,8 @@ Licensed under the Eiffel Forum License 2.
|
|||
|
||||
http://sopel.chat
|
||||
"""
|
||||
from __future__ import unicode_literals, absolute_import, print_function, division
|
||||
|
||||
import sys
|
||||
from tools import stderr
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
stderr('Error: Requires Python 2.7 or later. Try python2.7 sopel')
|
||||
sys.exit(1)
|
||||
if sys.version_info.major == 3 and sys.version_info.minor < 3:
|
||||
stderr('Error: When running on Python 3, Python 3.3 is required.')
|
||||
sys.exit(1)
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import signal
|
||||
|
@ -57,36 +47,71 @@ def main(argv=None):
|
|||
global homedir
|
||||
# Step One: Parse The Command Line
|
||||
try:
|
||||
parser = argparse.ArgumentParser(description='Sopel IRC Bot',
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Sopel IRC Bot',
|
||||
usage='%(prog)s [options]')
|
||||
parser.add_argument('-c', '--config', metavar='filename',
|
||||
parser.add_argument(
|
||||
'-c',
|
||||
'--config',
|
||||
metavar='filename',
|
||||
help='use a specific configuration file')
|
||||
parser.add_argument("-d", '--fork', action="store_true",
|
||||
dest="daemonize", help="Daemonize sopel")
|
||||
parser.add_argument("-q", '--quit', action="store_true", dest="quit",
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
'--fork',
|
||||
action="store_true",
|
||||
dest="daemonize",
|
||||
help="Daemonize sopel")
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
'--quit',
|
||||
action="store_true",
|
||||
dest="quit",
|
||||
help="Gracefully quit Sopel")
|
||||
parser.add_argument("-k", '--kill', action="store_true", dest="kill",
|
||||
parser.add_argument(
|
||||
"-k",
|
||||
'--kill',
|
||||
action="store_true",
|
||||
dest="kill",
|
||||
help="Kill Sopel")
|
||||
parser.add_argument("-l", '--list', action="store_true",
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
'--list',
|
||||
action="store_true",
|
||||
dest="list_configs",
|
||||
help="List all config files found")
|
||||
parser.add_argument("-m", '--migrate', action="store_true",
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
'--migrate',
|
||||
action="store_true",
|
||||
dest="migrate_configs",
|
||||
help="Migrate config files to the new format")
|
||||
parser.add_argument('--quiet', action="store_true", dest="quiet",
|
||||
parser.add_argument(
|
||||
'--quiet',
|
||||
action="store_true",
|
||||
dest="quiet",
|
||||
help="Supress all output")
|
||||
parser.add_argument('-w', '--configure-all', action='store_true',
|
||||
dest='wizard', help='Run the configuration wizard.')
|
||||
parser.add_argument('--configure-modules', action='store_true',
|
||||
dest='mod_wizard', help=(
|
||||
'Run the configuration wizard, but only for the '
|
||||
'module configuration options.'))
|
||||
parser.add_argument('-v', '--version', action="store_true",
|
||||
dest="version", help="Show version number and exit")
|
||||
parser.add_argument(
|
||||
'-w',
|
||||
'--configure-all',
|
||||
action='store_true',
|
||||
dest='wizard',
|
||||
help='Run the configuration wizard.')
|
||||
parser.add_argument(
|
||||
'--configure-modules',
|
||||
action='store_true',
|
||||
dest='mod_wizard',
|
||||
help='Run the configuration wizard, but only for the module ' + \
|
||||
'configuration options.')
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--version',
|
||||
action="store_true",
|
||||
dest="version",
|
||||
help="Show version number and exit")
|
||||
if argv:
|
||||
opts = parser.parse_args(argv)
|
||||
args = parser.parse_args(argv)
|
||||
else:
|
||||
opts = parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
|
||||
# Step Two: "Do not run as root" checks.
|
||||
try:
|
||||
|
@ -100,21 +125,21 @@ def main(argv=None):
|
|||
stderr('Error: Do not run Sopel as Administrator.')
|
||||
sys.exit(1)
|
||||
|
||||
if opts.version:
|
||||
if args.version:
|
||||
py_ver = '%s.%s.%s' % (sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
sys.version_info.micro)
|
||||
print('Sopel %s (running on python %s)' % (__version__, py_ver))
|
||||
print('http://sopel.chat/')
|
||||
return
|
||||
elif opts.wizard:
|
||||
_wizard('all', opts.config)
|
||||
elif args.wizard:
|
||||
_wizard('all', args.config)
|
||||
return
|
||||
elif opts.mod_wizard:
|
||||
_wizard('mod', opts.config)
|
||||
elif args.mod_wizard:
|
||||
_wizard('mod', args.config)
|
||||
return
|
||||
|
||||
if opts.list_configs:
|
||||
if args.list_configs:
|
||||
configs = enumerate_configs()
|
||||
print('Config files in ~/.sopel:')
|
||||
if len(configs) is 0:
|
||||
|
@ -125,7 +150,7 @@ def main(argv=None):
|
|||
print('-------------------------')
|
||||
return
|
||||
|
||||
config_name = opts.config or 'default'
|
||||
config_name = args.config or 'default'
|
||||
|
||||
configpath = find_config(config_name)
|
||||
if not os.path.isfile(configpath):
|
||||
|
@ -147,17 +172,17 @@ def main(argv=None):
|
|||
|
||||
logfile = os.path.os.path.join(config_module.core.logdir, 'stdio.log')
|
||||
|
||||
config_module._is_daemonized = opts.daemonize
|
||||
config_module._is_daemonized = args.daemonize
|
||||
|
||||
sys.stderr = tools.OutputRedirect(logfile, True, opts.quiet)
|
||||
sys.stdout = tools.OutputRedirect(logfile, False, opts.quiet)
|
||||
sys.stderr = tools.OutputRedirect(logfile, True, args.quiet)
|
||||
sys.stdout = tools.OutputRedirect(logfile, False, args.quiet)
|
||||
|
||||
# Handle --quit, --kill and saving the PID to file
|
||||
pid_dir = config_module.core.pid_dir
|
||||
if opts.config is None:
|
||||
if args.config is None:
|
||||
pid_file_path = os.path.join(pid_dir, 'sopel.pid')
|
||||
else:
|
||||
basename = os.path.basename(opts.config)
|
||||
basename = os.path.basename(args.config)
|
||||
if basename.endswith('.cfg'):
|
||||
basename = basename[:-4]
|
||||
pid_file_path = os.path.join(pid_dir, 'sopel-%s.pid' % basename)
|
||||
|
@ -168,28 +193,28 @@ def main(argv=None):
|
|||
except ValueError:
|
||||
old_pid = None
|
||||
if old_pid is not None and tools.check_pid(old_pid):
|
||||
if not opts.quit and not opts.kill:
|
||||
if not args.quit and not args.kill:
|
||||
stderr('There\'s already a Sopel instance running with this config file')
|
||||
stderr('Try using the --quit or the --kill options')
|
||||
sys.exit(1)
|
||||
elif opts.kill:
|
||||
elif args.kill:
|
||||
stderr('Killing the sopel')
|
||||
os.kill(old_pid, signal.SIGKILL)
|
||||
sys.exit(0)
|
||||
elif opts.quit:
|
||||
elif args.quit:
|
||||
stderr('Signaling Sopel to stop gracefully')
|
||||
if hasattr(signal, 'SIGUSR1'):
|
||||
os.kill(old_pid, signal.SIGUSR1)
|
||||
else:
|
||||
os.kill(old_pid, signal.SIGTERM)
|
||||
sys.exit(0)
|
||||
elif opts.kill or opts.quit:
|
||||
elif args.kill or args.quit:
|
||||
stderr('Sopel is not running!')
|
||||
sys.exit(1)
|
||||
elif opts.quit or opts.kill:
|
||||
elif args.quit or args.kill:
|
||||
stderr('Sopel is not running!')
|
||||
sys.exit(1)
|
||||
if opts.daemonize:
|
||||
if args.daemonize:
|
||||
child_pid = os.fork()
|
||||
if child_pid is not 0:
|
||||
sys.exit()
|
||||
|
|
Loading…
Reference in New Issue
Block a user