"usage": "!osu <option>\n" "Options:\n" " set <username>\n" " get\n" " notify-channel [channel]\n", "desc": "Handle osu! commands.\n" "`set` assigns your osu! user for notifying.\n" "`get` returns an osu! userpage link..\n" "~~`notify-channel` sets a channel as the notify channel. This channel should not be used by any " "member. Specify no channel to disable. **Requires `Manage Server` permission.**~~" } } osu = Config("osu", data={"key": "change to your api key", "profiles": {}}) osu_tracking = { } # Saves the requested data or deletes whenever the user stops playing (for comparisons) update_interval = 30 # Seconds logging_interval = 30 # Minutes request_limit = 100 osu_api = "https://osu.ppy.sh/api/" logging.getLogger("requests").setLevel(logging.WARNING) def calculate_acc(c50, c100, c300, miss): total_points_of_hits = int(c50) * 50 + int(c100) * 100 + int(c300) * 300 total_number_of_hits = int(miss) + int(c50) + int(c100) + int(c300)
import discord 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 str(message.author.id) not in aliases.data: aliases.data[str(message.author.id)] = {} # Set options aliases.data[str(message.author.id)][trigger if case_sensitive else trigger.lower()] = dict( text=text,
try: from PIL import Image except: resize = False logging.warning( "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, 3] # 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):
"usage": "!twitch <option>\n" "Options:\n" " set <username>\n" " get\n" " notify-channel [channel]", "desc": "Handle twitch commands.\n" "`set` assigns your twitch account for notifying.\n" "`get` returns a twitch channel link.\n" "~~`notify-channel` sets a channel as the notify channel. This channel should not be used by any " "member. Specify no channel to disable. **Requires `Manage Server` permission.**~~" } } twitch_channels = Config("twitch-channels", data={"channels": {}}) live_channels = {} update_interval = 180 # Seconds twitch_api = "https://api.twitch.tv/kraken" logging.getLogger("requests").setLevel(logging.WARNING) @asyncio.coroutine def on_ready(client: discord.Client): while not client.is_closed: try: yield from asyncio.sleep(update_interval) # Go through all set channels (if they're online on discord) and update their status
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.mention} rolls `{1}`.".format(message.author, rolled)) @plugins.command() async def avatar(message: discord.Message, member: Annotate.Member = Annotate.Self):
# 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
""" import re from collections import namedtuple, deque from traceback import print_exc from typing import Dict from urllib import parse as url_parse 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(format="bestaudio/best", extractaudio=True, audioformat="mp3", noplaylist=True, default_search="auto", quiet=True, nocheckcertificate=True) ffmpeg_before_options = "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5" 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():
from plugins.osulib import api, Mods 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* 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 = 30 # The pause time in seconds between updates time_elapsed = 0 # The registered time it takes to process all information between updates (changes each update)
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
""" 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.**"
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
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 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.
""" Plugin for compiling and executing brainfuck code. """ import asyncio import discord import plugins from pcbot import Annotate, Config client = plugins.client # type: discord.Client cfg = Config("brainfuck", data={}) # Keys are names and values are dict with author, code max_iterations = 2**17 brainfuck_chars = "+-><][.," class Loop: def __init__(self, start, end): self.start = start self.end = end self.pointer = None def set_pointer(self, pointer): self.pointer = (pointer.cursor, pointer.value) def compare_pointer(self, pointer): if self.pointer is None: return False return self.pointer == (pointer.cursor, pointer.value)
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()
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.guild_command_prefix(message.guild) # Display the specific command if command: if command.startswith(command_prefix): command = command[len(command_prefix):]
logs_from_limit = 5000 max_summaries = 5 update_task = asyncio.Event() 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_regex valid_member_silent = re.compile(r"@\((?P<name>.+)\)") valid_channel = utils.channel_mention_regex valid_options = ("+re", "+regex", "+case", "+tts", "+nobot", "+bot") 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() # Download logged messages
Commands: pasta """ from random import choice from difflib import get_close_matches import discord import asyncio from pcbot import Config, Annotate import plugins client = plugins.client # type: discord.Client pastas = Config("pastas", data={}) @plugins.command(aliases="paste") async def pasta(message: discord.Message, name: Annotate.LowerContent): """ Use copypastas. Don't forget to enclose the copypasta in quotes: `"pasta goes here"` for multiline pasta action. You also need quotes around `<name>` if it has any spaces. """ # Display a random pasta assert not name == ".", choice(list(pastas.data.values())) # We don't use spaces in pastas at all parsed_name = name.replace(" ", "") # Pasta might not be in the set assert parsed_name in pastas.data, "Pasta `{}` is undefined.\nPerhaps you meant: `{}`?".format( name, ", ".join(get_close_matches(parsed_name, pastas.data.keys(), cutoff=0.5)))
More to come? Commands: twitch """ import discord import logging from datetime import datetime 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(servers={})) @plugins.command(name="twitch") async def twitch_group(message: discord.Message, _: utils.placeholder): """ Administrative commands for twitch functions. Notifies when discord says you're streaming. """ pass @twitch_group.command(name="channels", permissions="manage_server") async def notify_channels(message: discord.Message, *channels: discord.Channel): """ Specify channels to notify when a member goes live, or use no arguments to disable. """ if message.server.id not in twitch_config.data["servers"]: twitch_config.data["servers"][message.server.id] = {}
""" 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.
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