More to come? Commands: twitch """ import discord import logging from datetime import datetime, timedelta from pcbot import utils, Config import plugins from plugins.twitchlib import twitch client = plugins.client # type: discord.Client twitch_config = Config("twitch-config", data=dict(guilds={})) # Keep track of all {member.id: date} that are streaming stream_history = {} repeat_notification_delta = timedelta(hours=2) async def on_reload(name): global stream_history local_history = stream_history await plugins.reload(name) stream_history = local_history
command specific functions and helpers. """ import re import shlex from enum import Enum from functools import wraps from io import BytesIO import aiohttp import discord from asyncio import subprocess as sub from pcbot import Config, config owner_cfg = Config("owner") member_mention_regex = re.compile(r"<@!?(?P<id>\d+)>") channel_mention_regex = re.compile(r"<#(?P<id>\d+)>") markdown_code_regex = re.compile( r"^(?P<capt>`*)(?:[a-z]+\n)?(?P<code>.+)(?P=capt)$", flags=re.DOTALL) identifier_prefix = re.compile(r"[a-zA-Z_]") client = None # Declare the Client. For python 3.6: client: discord.Client def set_client(c: discord.Client): """ Assign the client to a variable. """ global client client = c
mute unmute timeout suspend """ from collections import defaultdict import discord import asyncio from pcbot import Config, utils, Annotate import plugins client = plugins.client # type: discord.Client moderate = Config("moderate", data=defaultdict(dict)) default_config = {} # Used by add_setting helper function def setup_default_config(server: discord.Server): """ Setup default settings for a server. """ # Set to defaults if there is no config for the server if server.id not in moderate.data: moderate.data[server.id] = default_config moderate.save() return # Set to defaults if server's config is missing values if not all(k in moderate.data[server.id].keys() for k in default_config): moderate.data[server.id] = default_config moderate.save()
""" API wrapper for twitch.tv. """ import re import discord from pcbot import utils, Config twitch_config = Config("twitch-api", data=dict(ids={}, client_id=None)) # Define twitch API info client_id = twitch_config.data["client_id"] or "" api_url = "https://api.twitch.tv/kraken/" url_pattern = re.compile(r"^https://www.twitch.tv/(?P<name>.+)$") class RequestFailed(Exception): """ For when the api request fails. """ pass class UserNotResolved(Exception): """ For when a name isn't resolved. """ pass async def request(endpoint: str = None, **params): """ Perform a request using the twitch kraken v5 API. If the url key is not given, the request is sent to the root URL.
import discord import pendulum from pytz import all_timezones import plugins from pcbot import Config, Annotate client = plugins.client # type: discord.Client time_cfg = Config("time", data=dict(countdown={}, timezone={})) dt_format = "%A, %d %B %Y %H:%M:%S" @plugins.argument() def tz_arg(timezone: str): """ Get timezone from a string. """ for tz in all_timezones: if tz.lower().endswith(timezone.lower()): return tz return None def reverse_gmt(timezone: str): """ POSIX is stupid so these are reversed. """ if "+" in timezone: timezone = timezone.replace("+", "-") elif "-" in timezone: timezone = timezone.replace("-", "+") return timezone
import importlib import inspect import logging import random from datetime import datetime, timedelta import discord import asyncio from pcbot import utils, Config, Annotate, config import plugins client = plugins.client # type: discord.Client sub = asyncio.subprocess lambdas = Config("lambdas", data={}) lambda_config = Config("lambda-config", data=dict(imports=[], blacklist=[])) code_globals = {} @plugins.command(name="help", aliases="commands") async def help_(message: discord.Message, command: str.lower=None, *args): """ Display commands or their usage and description. """ command_prefix = config.server_command_prefix(message.server) # Display the specific command if command: if command.startswith(command_prefix): command = command[len(command_prefix):]
Commands: music """ from collections import namedtuple, deque from traceback import print_exc from typing import Dict import asyncio import discord import plugins from pcbot import utils, Annotate, Config client = plugins.client # type: discord.Client music_channels = Config("music_channels", data=[]) voice_states = {} # type: Dict[discord.Server, VoiceState] youtube_dl_options = dict(default_search="auto", quiet=True, nocheckcertificate=True) max_songs_queued = 2 # How many songs each member are allowed in the queue at once max_song_length = 60 * 120 # The maximum song length in seconds default_volume = .6 if not discord.opus.is_loaded(): discord.opus.load_opus('libopus-0.x64.dll') Song = namedtuple("Song", "channel player requester")
from pcbot import owner, Annotate, Config, get_command, format_exception commands = { "help": "!help [command]", "setowner": None, "stop": "!stop", "game": "!game <name ...>", "do": "!do <python code ...>", "eval": "!eval <expression ...>", "plugin": "!plugin [reload | load | unload] [plugin]", "lambda": "!lambda [add <trigger> <python code> | [remove | enable | disable | source] <trigger>]" } lambdas = Config("lambdas", data={}) lambda_blacklist = [] def get_formatted_code(code): """ Format code from markdown format. This will filter out markdown code and give the executable python code, or return a string that would raise an error when it's executed by exec() or eval(). """ match = re.match(r"^(?P<capt>`*)(?:[a-z]+\n)?(?P<code>.+)(?P=capt)$", code, re.DOTALL) if match: code = match.group("code") if not code == "`": return code
import logging import re from collections import namedtuple import discord import plugins from pcbot import Config client = plugins.client # type: discord.Client blacklist = Config("blacklist", data={ "enabled": False, "global": {}, "server": [], "channel": [] }, pretty=True) blacklist_config_fieldnames = [ "match_patterns", "regex_patterns", "case_sensitive", "response", "bots", "exclude", "words", "id", "override" ] BlacklistConfig = namedtuple("BlacklistConfig", " ".join(blacklist_config_fieldnames)) blacklist_cache = {} def make_config_object(data: dict): """ Return a BlacklistConfig from the given dict.
import asyncio from pcbot import Config, Annotate, config, utils import plugins client = plugins.client # type: discord.Client alias_desc = \ "Assign an alias command, where trigger is the command in it's entirety: `{pre}cmd` or `>cmd` or `cmd`.\n" \ "Feel free to use spaces in a **trigger** by *enclosing it with quotes*, like so: `\"{pre}my cmd\"`.\n\n" \ "**Options**:\n" \ "`-anywhere` makes the alias trigger anywhere in a message, and not just the start of a message.\n" \ "`-case-sensitive` ensures that you *need* to follow the same casing.\n" \ "`-delete-message` removes the original message. This option can not be mixed with the `-anywhere` option.\n" \ aliases = Config("user_alias", data={}) @plugins.command(description=alias_desc, pos_check=lambda s: s.startswith("-")) async def alias(message: discord.Message, *options: str.lower, trigger: str, text: Annotate.Content): """ Assign an alias. Description is defined in alias_desc. """ anywhere = "-anywhere" in options case_sensitive = "-case-sensitive" in options delete_message = not anywhere and "-delete-message" in options if message.author.id not in aliases.data: aliases.data[message.author.id] = {} # Set options aliases.data[message.author.id][trigger if case_sensitive else trigger.
update_task.set() # Define some regexes for option checking in "summary" command valid_num = re.compile(r"\*(?P<num>\d+)") valid_member = utils.member_mention_pattern valid_member_silent = re.compile(r"@\((?P<name>.+)\)") valid_role = re.compile(r"<@&(?P<id>\d+)>") valid_channel = utils.channel_mention_pattern valid_options = ("+re", "+regex", "+case", "+tts", "+nobot", "+bot", "+coherent", "+strict") on_no_messages = "**There were no messages to generate a summary from, {0.author.name}.**" on_fail = "**I was unable to construct a summary, {0.author.name}.**" summary_options = Config("summary_options", data=dict(no_bot=False, no_self=False), pretty=True) async def update_messages(channel: discord.Channel): """ Download messages. """ messages = stored_messages[channel.id] # type: deque # We only want to log messages when there are none # Any messages after this logging will be logged in the on_message event if messages: return # Make sure not to download messages twice by setting this handy task update_task.clear()
# Define some regexes for option checking in "summary" command valid_num = re.compile(r"\*(?P<num>\d+)") valid_member = utils.member_mention_pattern valid_member_silent = re.compile(r"@\((?P<name>.+)\)") valid_role = re.compile(r"<@&(?P<id>\d+)>") valid_channel = utils.channel_mention_pattern valid_options = ("+re", "+regex", "+case", "+tts", "+nobot", "+bot", "+coherent", "+loose") on_no_messages = "**There were no messages to generate a summary from, {0.author.name}.**" on_fail = "**I was unable to construct a summary, {0.author.name}.**" summary_options = Config("summary_options", data=dict(no_bot=False, no_self=False, persistent_channels=[]), pretty=True) summary_data = Config("summary_data", data=dict(channels={})) def to_persistent(message: discord.Message): return dict(content=message.clean_content, author=str(message.author.id), bot=message.author.bot) async def update_messages(channel: discord.TextChannel): """ Download messages. """ messages = stored_messages[str(channel.id)] # type: deque
try: from PIL import Image except: resize = False logging.warn( "PIL could not be loaded. The pokedex works like usual, however sprites will remain 1x scaled." ) else: resize = True client = plugins.client # type: discord.Client api_path = "plugins/pokedexlib/pokedex.json" sprites_path = "plugins/pokedexlib/sprites/" pokedex_config = Config("pokedex", data=defaultdict(dict)) default_scale_factor = 1.8 min_scale_factor, max_scale_factor = 0.25, 4 pokemon_go_gen = [1, 2] # Load the Pokedex API with open(api_path) as api_file: api = json.load(api_file) pokedex = api["pokemon"] # Load all our sprites into RAM (they don't take much space) # Unlike the pokedex.json API, these use pokemon ID as keys. # The values are the sprites in bytes. sprites = {} for file in os.listdir(sprites_path):
""" Would you rather? This plugin includes would you rather functionality """ import asyncio import random import re import discord import plugins from pcbot import Config client = plugins.client # type: discord.Client db = Config("would-you-rather", data=dict(timeout=10, responses=["**{name}** would **{choice}**!"], questions=[]), pretty=True) command_pattern = re.compile(r"(.+)(?:\s+or|\s*,)\s+([^?]+)\?*") sessions = set() # All running would you rather's are in this set @plugins.argument( "{open}option ...{close} or/, {open}other option ...{close}[?]", allow_spaces=True) async def options(arg): """ Command argument for receiving two options. """ match = command_pattern.match(arg) assert match assert not match.group(1).lower() == match.group( 2).lower(), "**The choices cannot be the same.**"
Commands: roll feature """ import random from re import match from datetime import datetime, timedelta import discord from pcbot import utils, Config, Annotate import plugins client = plugins.client # type: discord.Client feature_reqs = Config(filename="feature_requests", data={}) # cleverbot = Cleverbot(config.name.replace(" ", "_") + "-discord-bot") @plugins.command() async def roll(message: discord.Message, num: utils.int_range(f=1) = 100): """ Roll a number from 1-100 if no second argument or second argument is not a number. Alternatively rolls `num` times (minimum 1). """ rolled = random.randint(1, num) await client.say( message, "**{0.display_name}** rolls `{1}`.".format(message.author, rolled)) @plugins.command() async def avatar(message: discord.Message,
from plugins.twitchlib import twitch client = plugins.client # type: discord.Client # Configuration data for this plugin, including settings for members and the API key osu_config = Config( "osu", pretty=True, data=dict( key="change to your api key", pp_threshold=0.13, # The amount of pp gain required to post a score score_request_limit= 100, # The maximum number of scores to request, between 0-100 minimum_pp_required= 0, # The minimum pp required to assign a gamemode/profile in general use_mentions_in_scores= True, # Whether the bot will mention people when they set a *score* update_interval=30, # The sleep time in seconds between updates profiles={}, # Profile setup as member_id: osu_id mode={}, # Member's game mode as member_id: gamemode_value server= {}, # Server specific info for score- and map notification channels update_mode= {}, # Member's notification update mode as member_id: UpdateModes.name primary_server= {}, # Member's primary server; defines where they should be mentioned: member_id: server_id )) osu_tracking = { } # Saves the requested data or deletes whenever the user stops playing (for comparisons) update_interval = osu_config.data.get("update_interval", 30) time_elapsed = 0 # The registered time it takes to process all information between updates (changes each update)
Commands: pasta """ from copy import copy from difflib import get_close_matches from random import choice import discord import asyncio from pcbot import Config, Annotate, convert_to_embed import plugins client = plugins.client # type: discord.Client pastas = Config("pastas", data={}) pasta_cache = {} # list of generate_pasta tuples to cache embed_color = discord.Color.dark_grey() async def generate_pasta(name: str): """ Generate a pasta embed. """ # Return the optionally cached result if name in pasta_cache: return pasta_cache[name] # Choose a random pasta when the name is . if name == ".": name = choice(list(pastas.data.values())) # Remove spaces as pastas are space independent