mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 11:18:54 -05:00
527 lines
20 KiB
Python
527 lines
20 KiB
Python
import discord
|
|
from discord.ext import commands
|
|
from redbot.core import Config, checks, RedContext
|
|
from redbot.core.utils.chat_formatting import pagify, box
|
|
from redbot.core.bot import Red
|
|
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity
|
|
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidCredentials,
|
|
CommunityNotFound, OfflineCommunity, StreamsError)
|
|
from . import streamtypes as StreamClasses
|
|
from collections import defaultdict
|
|
import asyncio
|
|
|
|
CHECK_DELAY = 60
|
|
|
|
|
|
class Streams:
|
|
|
|
global_defaults = {
|
|
"tokens": {},
|
|
"streams": [],
|
|
"communities": []
|
|
}
|
|
|
|
guild_defaults = {
|
|
"autodelete": False,
|
|
"mention_everyone": False,
|
|
"mention_here": False
|
|
}
|
|
|
|
role_defaults = {
|
|
"mention": False
|
|
}
|
|
|
|
def __init__(self, bot: Red):
|
|
self.db = Config.get_conf(self, 26262626)
|
|
|
|
self.db.register_global(**self.global_defaults)
|
|
|
|
self.db.register_guild(**self.guild_defaults)
|
|
|
|
self.db.register_role(**self.role_defaults)
|
|
|
|
self.bot = bot
|
|
|
|
self.bot.loop.create_task(self._initialize_lists())
|
|
|
|
async def _initialize_lists(self):
|
|
self.streams = await self.load_streams()
|
|
self.communities = await self.load_communities()
|
|
|
|
self.task = self.bot.loop.create_task(self._stream_alerts())
|
|
|
|
@commands.command()
|
|
async def twitch(self, ctx, channel_name: str):
|
|
"""Checks if a Twitch channel is streaming"""
|
|
token = await self.db.tokens.get_attr(TwitchStream.__name__)
|
|
stream = TwitchStream(name=channel_name,
|
|
token=token)
|
|
await self.check_online(ctx, stream)
|
|
|
|
@commands.command()
|
|
async def hitbox(self, ctx, channel_name: str):
|
|
"""Checks if a Hitbox channel is streaming"""
|
|
stream = HitboxStream(name=channel_name)
|
|
await self.check_online(ctx, stream)
|
|
|
|
@commands.command()
|
|
async def mixer(self, ctx, channel_name: str):
|
|
"""Checks if a Mixer channel is streaming"""
|
|
stream = MixerStream(name=channel_name)
|
|
await self.check_online(ctx, stream)
|
|
|
|
@commands.command()
|
|
async def picarto(self, ctx, channel_name: str):
|
|
"""Checks if a Picarto channel is streaming"""
|
|
stream = PicartoStream(name=channel_name)
|
|
await self.check_online(ctx, stream)
|
|
|
|
async def check_online(self, ctx, stream):
|
|
try:
|
|
embed = await stream.is_online()
|
|
except OfflineStream:
|
|
await ctx.send("The stream is offline.")
|
|
except StreamNotFound:
|
|
await ctx.send("The channel doesn't seem to exist.")
|
|
except InvalidCredentials:
|
|
await ctx.send("The twitch token is either invalid or has not been set. "
|
|
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
|
except APIError:
|
|
await ctx.send("Something went wrong whilst trying to contact the "
|
|
"stream service's API.")
|
|
else:
|
|
await ctx.send(embed=embed)
|
|
|
|
@commands.group()
|
|
@commands.guild_only()
|
|
@checks.mod()
|
|
async def streamalert(self, ctx):
|
|
if ctx.invoked_subcommand is None:
|
|
await ctx.send_help()
|
|
|
|
@streamalert.group(name="twitch")
|
|
async def _twitch(self, ctx):
|
|
"""Twitch stream alerts"""
|
|
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
|
|
await ctx.send_help()
|
|
|
|
@_twitch.command(name="channel")
|
|
async def twitch_alert_channel(self, ctx: RedContext, channel_name: str):
|
|
"""Sets a Twitch stream alert notification in the channel"""
|
|
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
|
|
|
@_twitch.command(name="community")
|
|
async def twitch_alert_community(self, ctx: RedContext, community: str):
|
|
"""Sets a Twitch stream alert notification in the channel
|
|
for the specified community."""
|
|
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
|
|
|
@streamalert.command(name="hitbox")
|
|
async def hitbox_alert(self, ctx, channel_name: str):
|
|
"""Sets a Hitbox stream alert notification in the channel"""
|
|
await self.stream_alert(ctx, HitboxStream, channel_name)
|
|
|
|
@streamalert.command(name="mixer")
|
|
async def mixer_alert(self, ctx, channel_name: str):
|
|
"""Sets a Mixer stream alert notification in the channel"""
|
|
await self.stream_alert(ctx, MixerStream, channel_name)
|
|
|
|
@streamalert.command(name="picarto")
|
|
async def picarto_alert(self, ctx, channel_name: str):
|
|
"""Sets a Picarto stream alert notification in the channel"""
|
|
await self.stream_alert(ctx, PicartoStream, channel_name)
|
|
|
|
@streamalert.command(name="stop")
|
|
async def streamalert_stop(self, ctx, _all: bool=False):
|
|
"""Stops all stream notifications in the channel
|
|
|
|
Adding 'yes' will disable all notifications in the server"""
|
|
streams = self.streams.copy()
|
|
local_channel_ids = [c.id for c in ctx.guild.channels]
|
|
to_remove = []
|
|
|
|
for stream in streams:
|
|
for channel_id in stream.channels:
|
|
if channel_id == ctx.channel.id:
|
|
stream.channels.remove(channel_id)
|
|
elif _all and ctx.channel.id in local_channel_ids:
|
|
if channel_id in stream.channels:
|
|
stream.channels.remove(channel_id)
|
|
|
|
if not stream.channels:
|
|
to_remove.append(stream)
|
|
|
|
for stream in to_remove:
|
|
streams.remove(stream)
|
|
|
|
self.streams = streams
|
|
await self.save_streams()
|
|
|
|
msg = "All {}'s stream alerts have been disabled." \
|
|
"".format("server" if _all else "channel")
|
|
|
|
await ctx.send(msg)
|
|
|
|
@streamalert.command(name="list")
|
|
async def streamalert_list(self, ctx):
|
|
streams_list = defaultdict(list)
|
|
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
|
msg = "Active stream alerts:\n\n"
|
|
|
|
for stream in self.streams:
|
|
for channel_id in stream.channels:
|
|
if channel_id in guild_channels_ids:
|
|
streams_list[channel_id].append(stream.name.lower())
|
|
|
|
if not streams_list:
|
|
await ctx.send("There are no active stream alerts in this server.")
|
|
return
|
|
|
|
for channel_id, streams in streams_list.items():
|
|
channel = ctx.guild.get_channel(channel_id)
|
|
msg += "** - #{}**\n{}\n".format(channel, ", ".join(streams))
|
|
|
|
for page in pagify(msg):
|
|
await ctx.send(page)
|
|
|
|
async def stream_alert(self, ctx, _class, channel_name):
|
|
stream = self.get_stream(_class, channel_name.lower())
|
|
if not stream:
|
|
token = await self.db.tokens.get_attr(_class.__name__)
|
|
stream = _class(name=channel_name,
|
|
token=token)
|
|
try:
|
|
exists = await self.check_exists(stream)
|
|
except InvalidCredentials:
|
|
await ctx.send("The twitch token is either invalid or has not been set. "
|
|
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
|
return
|
|
except APIError:
|
|
await ctx.send("Something went wrong whilst trying to contact the "
|
|
"stream service's API.")
|
|
return
|
|
else:
|
|
if not exists:
|
|
await ctx.send("That channel doesn't seem to exist.")
|
|
return
|
|
|
|
await self.add_or_remove(ctx, stream)
|
|
|
|
async def community_alert(self, ctx, _class, community_name):
|
|
community = self.get_community(_class, community_name)
|
|
if not community:
|
|
token = await self.db.tokens.get_attr(_class.__name__)
|
|
community = _class(name=community_name, token=token)
|
|
try:
|
|
await community.get_community_streams()
|
|
except InvalidCredentials:
|
|
await ctx.send(
|
|
"The twitch token is either invalid or has not been set. "
|
|
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
|
return
|
|
except CommunityNotFound:
|
|
await ctx.send("That community doesn't seem to exist.")
|
|
return
|
|
except APIError:
|
|
await ctx.send(
|
|
"Something went wrong whilst trying to contact the "
|
|
"stream service's API.")
|
|
return
|
|
except OfflineCommunity:
|
|
pass
|
|
|
|
await self.add_or_remove_community(ctx, community)
|
|
|
|
@commands.group()
|
|
@checks.mod()
|
|
async def streamset(self, ctx):
|
|
if ctx.invoked_subcommand is None:
|
|
await ctx.send_help()
|
|
|
|
@streamset.command()
|
|
@checks.is_owner()
|
|
async def twitchtoken(self, ctx, token: str):
|
|
"""Set the Client ID for twitch.
|
|
|
|
To do this, follow these steps:
|
|
1. Go to this page: https://dev.twitch.tv/dashboard/apps.
|
|
2. Click *Register Your Application*
|
|
3. Enter a name, set the OAuth Redirect URI to `http://localhost`, and
|
|
select an Application Category of your choosing.
|
|
4. Click *Register*, and on the following page, copy the Client ID.
|
|
5. Paste the Client ID into this command. Done!
|
|
|
|
"""
|
|
tokens = await self.db.tokens()
|
|
tokens["TwitchStream"] = token
|
|
tokens["TwitchCommunity"] = token
|
|
await self.db.tokens.set(tokens)
|
|
await ctx.send("Twitch token set.")
|
|
|
|
@streamset.group()
|
|
@commands.guild_only()
|
|
async def mention(self, ctx):
|
|
"""Sets mentions for stream alerts."""
|
|
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
|
|
await ctx.send_help()
|
|
|
|
@mention.command(aliases=["everyone"])
|
|
@commands.guild_only()
|
|
async def all(self, ctx):
|
|
"""Toggles everyone mention"""
|
|
guild = ctx.guild
|
|
current_setting = await self.db.guild(guild).mention_everyone()
|
|
if current_setting:
|
|
await self.db.guild(guild).mention_everyone.set(False)
|
|
await ctx.send("@\u200beveryone will no longer be mentioned "
|
|
"for a stream alert.")
|
|
else:
|
|
await self.db.guild(guild).mention_everyone.set(True)
|
|
await ctx.send("When a stream configured for stream alerts "
|
|
"comes online, @\u200beveryone will be mentioned")
|
|
|
|
@mention.command(aliases=["here"])
|
|
@commands.guild_only()
|
|
async def online(self, ctx):
|
|
"""Toggles here mention"""
|
|
guild = ctx.guild
|
|
current_setting = await self.db.guild(guild).mention_here()
|
|
if current_setting:
|
|
await self.db.guild(guild).mention_here.set(False)
|
|
await ctx.send("@\u200bhere will no longer be mentioned "
|
|
"for a stream alert.")
|
|
else:
|
|
await self.db.guild(guild).mention_here.set(True)
|
|
await ctx.send("When a stream configured for stream alerts "
|
|
"comes online, @\u200bhere will be mentioned")
|
|
|
|
@mention.command()
|
|
@commands.guild_only()
|
|
async def role(self, ctx, *, role: discord.Role):
|
|
"""Toggles role mention"""
|
|
current_setting = await self.db.role(role).mention()
|
|
if not role.mentionable:
|
|
await ctx.send("That role is not mentionable!")
|
|
return
|
|
if current_setting:
|
|
await self.db.role(role).mention.set(False)
|
|
await ctx.send("@\u200b{} will no longer be mentioned "
|
|
"for a stream alert".format(role.name))
|
|
else:
|
|
await self.db.role(role).mention.set(True)
|
|
await ctx.send("When a stream configured for stream alerts "
|
|
"comes online, @\u200b{} will be mentioned"
|
|
"".format(role.name))
|
|
|
|
@streamset.command()
|
|
@commands.guild_only()
|
|
async def autodelete(self, ctx, on_off: bool):
|
|
"""Toggles automatic deletion of notifications for streams that go offline"""
|
|
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
|
if on_off:
|
|
await ctx.send("The notifications will be deleted once "
|
|
"streams go offline.")
|
|
else:
|
|
await ctx.send("Notifications will never be deleted.")
|
|
|
|
async def add_or_remove(self, ctx, stream):
|
|
if ctx.channel.id not in stream.channels:
|
|
stream.channels.append(ctx.channel.id)
|
|
if stream not in self.streams:
|
|
self.streams.append(stream)
|
|
await ctx.send("I'll send a notification in this channel when {} "
|
|
"is online.".format(stream.name))
|
|
else:
|
|
stream.channels.remove(ctx.channel.id)
|
|
if not stream.channels:
|
|
self.streams.remove(stream)
|
|
await ctx.send("I won't send notifications about {} in this "
|
|
"channel anymore.".format(stream.name))
|
|
|
|
await self.save_streams()
|
|
|
|
async def add_or_remove_community(self, ctx, community):
|
|
if ctx.channel.id not in community.channels:
|
|
community.channels.append(ctx.channel.id)
|
|
if community not in self.communities:
|
|
self.communities.append(community)
|
|
await ctx.send("I'll send a notification in this channel when a "
|
|
"channel is streaming to the {} community"
|
|
"".format(community.name))
|
|
else:
|
|
community.channels.remove(ctx.channel.id)
|
|
if not community.channels:
|
|
self.communities.remove(community)
|
|
await ctx.send("I won't send notifications about channels streaming "
|
|
"to the {} community in this channel anymore"
|
|
"".format(community.name))
|
|
await self.save_communities()
|
|
|
|
def get_stream(self, _class, name):
|
|
for stream in self.streams:
|
|
# if isinstance(stream, _class) and stream.name == name:
|
|
# return stream
|
|
# Reloading this cog causes an issue with this check ^
|
|
# isinstance will always return False
|
|
# As a workaround, we'll compare the class' name instead.
|
|
# Good enough.
|
|
if stream.type == _class.__name__ and stream.name.lower() == name.lower():
|
|
return stream
|
|
|
|
def get_community(self, _class, name):
|
|
for community in self.communities:
|
|
if community.type == _class.__name__ and community.name.lower() == name.lower():
|
|
return community
|
|
|
|
async def check_exists(self, stream):
|
|
try:
|
|
await stream.is_online()
|
|
except OfflineStream:
|
|
pass
|
|
except StreamNotFound:
|
|
return False
|
|
except StreamsError:
|
|
raise
|
|
return True
|
|
|
|
async def _stream_alerts(self):
|
|
while True:
|
|
try:
|
|
await self.check_streams()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
try:
|
|
await self.check_communities()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
await asyncio.sleep(CHECK_DELAY)
|
|
|
|
async def check_streams(self):
|
|
for stream in self.streams:
|
|
try:
|
|
embed = await stream.is_online()
|
|
except OfflineStream:
|
|
for message in stream._messages_cache:
|
|
try:
|
|
autodelete = self.db.guild(message.guild).autodelete()
|
|
if autodelete:
|
|
await message.delete()
|
|
except:
|
|
pass
|
|
stream._messages_cache.clear()
|
|
except:
|
|
pass
|
|
else:
|
|
if stream._messages_cache:
|
|
continue
|
|
for channel_id in stream.channels:
|
|
channel = self.bot.get_channel(channel_id)
|
|
mention_str = await self._get_mention_str(channel.guild)
|
|
|
|
if mention_str:
|
|
content = "{}, {} is online!".format(mention_str, stream.name)
|
|
else:
|
|
content = "{} is online!".format(stream.name)
|
|
|
|
try:
|
|
m = await channel.send(content, embed=embed)
|
|
stream._messages_cache.append(m)
|
|
except:
|
|
pass
|
|
|
|
async def _get_mention_str(self, guild: discord.Guild):
|
|
settings = self.db.guild(guild)
|
|
mentions = []
|
|
if await settings.mention_everyone():
|
|
mentions.append('@everyone')
|
|
if await settings.mention_here():
|
|
mentions.append('@here')
|
|
for role in guild.roles:
|
|
if await self.db.role(role).mention():
|
|
mentions.append(role.mention)
|
|
return ' '.join(mentions)
|
|
|
|
async def check_communities(self):
|
|
for community in self.communities:
|
|
try:
|
|
streams = community.get_community_streams()
|
|
except CommunityNotFound:
|
|
print("Community {} not found!".format(community.name))
|
|
continue
|
|
except OfflineCommunity:
|
|
pass
|
|
else:
|
|
token = self.db.tokens().get(TwitchStream.__name__)
|
|
for channel in community.channels:
|
|
chn = self.bot.get_channel(channel)
|
|
await chn.send("Online streams for {}".format(community.name))
|
|
for stream in streams:
|
|
stream_obj = TwitchStream(
|
|
token=token, name=stream["channel"]["name"],
|
|
id=stream["_id"]
|
|
)
|
|
try:
|
|
emb = await stream_obj.is_online()
|
|
except:
|
|
pass
|
|
else:
|
|
for channel in community.channels:
|
|
chn = self.bot.get_channel(channel)
|
|
await chn.send(embed=emb)
|
|
|
|
async def load_streams(self):
|
|
streams = []
|
|
|
|
for raw_stream in await self.db.streams():
|
|
_class = getattr(StreamClasses, raw_stream["type"], None)
|
|
if not _class:
|
|
continue
|
|
|
|
token = await self.db.tokens.get_attr(_class.__name__)
|
|
streams.append(_class(token=token, **raw_stream))
|
|
|
|
# issue 1191 extended resolution: Remove this after suitable period
|
|
# Fast dedupe below
|
|
seen = set()
|
|
seen_add = seen.add
|
|
return [x for x in streams
|
|
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
|
|
|
# return streams
|
|
|
|
async def load_communities(self):
|
|
communities = []
|
|
|
|
for raw_community in await self.db.communities():
|
|
_class = getattr(StreamClasses, raw_community["type"], None)
|
|
if not _class:
|
|
continue
|
|
|
|
token = await self.db.tokens.get_attr(_class.__name__)
|
|
communities.append(_class(token=token, **raw_community))
|
|
|
|
# issue 1191 extended resolution: Remove this after suitable period
|
|
# Fast dedupe below
|
|
seen = set()
|
|
seen_add = seen.add
|
|
return [x for x in communities
|
|
if not (x.name.lower() in seen or seen_add(x.name.lower()))]
|
|
# return communities
|
|
|
|
async def save_streams(self):
|
|
raw_streams = []
|
|
for stream in self.streams:
|
|
raw_streams.append(stream.export())
|
|
|
|
await self.db.streams.set(raw_streams)
|
|
|
|
async def save_communities(self):
|
|
raw_communities = []
|
|
for community in self.communities:
|
|
raw_communities.append(community.export())
|
|
|
|
await self.db.communities.set(raw_communities)
|
|
|
|
def __unload(self):
|
|
self.task.cancel()
|