2024-01-06 13:54:31 -05:00
"""
MIT License
Copyright ( c ) 2018 - Present NeuroAssassin
Permission is hereby granted , free of charge , to any person obtaining a copy
of this software and associated documentation files ( the " Software " ) , to deal
in the Software without restriction , including without limitation the rights
to use , copy , modify , merge , publish , distribute , sublicense , and / or sell
copies of the Software , and to permit persons to whom the Software is
furnished to do so , subject to the following conditions :
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software .
THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR
IMPLIED , INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY ,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT . IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM , DAMAGES OR OTHER
LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM ,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE .
"""
# Huge thanks to Sinbad for allowing me to copy parts of his RSS cog
# (https://github.com/mikeshardmind/SinbadCogs/tree/v3/rss), which I
# used to grab the latest commits from repositories.
# Also, the code I use for updating repos I took directly from Red,
# and just took out the message interactions
import asyncio
import traceback
from datetime import datetime
2024-01-07 08:41:53 -05:00
from urllib . parse import urlparse
2024-01-06 13:54:31 -05:00
import aiohttp
import discord
2024-01-07 09:01:27 -05:00
from discord . ext import tasks
2024-01-06 13:54:31 -05:00
from redbot . cogs . downloader . repo_manager import Repo
from redbot . core import Config , commands
2024-01-07 08:30:02 -05:00
from redbot . core . utils . chat_formatting import box , humanize_list , inline
2024-01-06 13:54:31 -05:00
import feedparser
class UpdateChecker ( commands . Cog ) :
2024-01-07 08:48:35 -05:00
""" Get notices or auto-update cogs when an update is available for its repo """
2024-01-06 13:54:31 -05:00
def __init__ ( self , bot ) :
self . bot = bot
self . session = aiohttp . ClientSession ( )
self . conf = Config . get_conf ( self , identifier = 473541068378341376 )
default_global = {
" repos " : { } ,
" auto " : False ,
" gochannel " : 0 ,
" embed " : True ,
" whitelist " : [ ] ,
" blacklist " : [ ] ,
}
self . conf . register_global ( * * default_global )
self . task = self . bot . loop . create_task ( self . bg_task ( ) )
2024-01-07 14:20:21 -05:00
async def cog_unload ( self ) :
2024-01-06 13:54:31 -05:00
self . __unload ( )
def __unload ( self ) :
self . task . cancel ( )
self . session . detach ( )
2024-01-07 14:20:21 -05:00
async def red_delete_data_for_user ( self , * * kwargs ) : # pylint: disable=unused-argument
2024-01-06 13:54:31 -05:00
""" This cog does not store user data """
return
2024-01-07 09:01:27 -05:00
@tasks.loop ( minutes = 1 )
2024-01-06 13:54:31 -05:00
async def bg_task ( self ) :
2024-01-07 14:27:19 -05:00
cog = self . bot . get_cog ( " Downloader " )
if cog is not None :
data = await self . conf . all ( )
repos = data [ " repos " ]
channel = data [ " gochannel " ]
2024-01-08 01:06:30 -05:00
auto = data [ " auto " ]
2024-01-07 14:27:19 -05:00
use_embed = data [ " embed " ]
whitelist = data [ " whitelist " ]
blacklist = data [ " blacklist " ]
if channel :
channel = self . bot . get_channel ( channel )
if channel is None :
await self . bot . send_to_owners (
" [Update Checker] It appears that I am no longer allowed to send messages to the designated update channel. "
" From now on, it will DM you. "
)
await self . conf . gochannel . set ( 0 )
send = self . bot . send_to_owners
else :
use_embed = (
use_embed and channel . permissions_for ( channel . guild . me ) . embed_links
)
send = channel . send
else :
send = self . bot . send_to_owners
all_repos = cog . _repo_manager . get_all_repo_names ( ) # pylint: disable=protected-access
for repo in all_repos :
if not ( repo in list ( repos . keys ( ) ) ) :
repos [ repo ] = " --default-- "
await self . conf . repos . set ( repos )
saving_dict = { k : v for k , v in repos . items ( ) if k in all_repos }
for repo_name , commit_saved in saving_dict . items ( ) :
repo = cog . _repo_manager . get_repo ( repo_name ) # pylint: disable=protected-access
if not repo :
continue
parsed_url = urlparse ( repo . url )
if parsed_url . netloc == " github.com " :
url = repo . url + r " /commits/ " + repo . branch + " .atom "
response = await self . fetch_feed ( url )
try :
commit = response . entries [ 0 ] [ " id " ] [ 33 : ]
chash = " [ " + commit + " ]( " + response . entries [ 0 ] [ " link " ] + " ) "
cn = response . entries [ 0 ] [ " title " ] + " - " + response . entries [ 0 ] [ " author " ]
image = response . entries [ 0 ] [ " media_thumbnail " ] [ 0 ] [ " url " ] . split ( " ? " ) [ 0 ]
except AttributeError :
continue
else :
url = repo . url + r " /rss/branch/ " + repo . branch
response = await self . fetch_feed ( url )
try :
commit = response . entries [ 0 ] [ " id " ] [ 33 : ]
chash = " [ " + commit + " ]( " + response . entries [ 0 ] [ " link " ] + " ) "
cn = response . entries [ 0 ] [ " title " ] + " - " + response . entries [ 0 ] [ " author " ]
image = await self . fetch_gitea_thumbnail ( parsed_url . scheme + " :// " + parsed_url . netloc + " /api/v1/repos " + parsed_url . path )
except AttributeError :
continue
saving_dict [ repo_name ] = commit
if whitelist :
if repo_name not in whitelist :
continue
if repo_name in blacklist :
continue
# CN is used here for backwards compatability, don't want people to get an
# update for each and every one of their cogs when updating this cog
if commit_saved not in ( commit , cn , ' --default-- ' ) :
if use_embed :
e = discord . Embed (
title = " Update Checker " ,
description = f " Update available for repo: { repo . name } " ,
timestamp = datetime . utcnow ( ) ,
color = 0x00FF00 ,
)
e . add_field ( name = " URL " , value = repo . url )
e . add_field ( name = " Branch " , value = repo . branch )
e . add_field ( name = " Commit " , value = cn )
e . add_field ( name = " Hash " , value = chash )
if image is not None :
e . set_thumbnail ( url = image )
else :
e = box (
" [Update Checker] "
f " Repo: { repo . name } \n "
f " URL: { repo . url } \n "
f " Commit: { cn } \n "
f " Hash: { commit } \n "
f " Time: { datetime . utcnow ( ) } " ,
' css '
)
try :
if use_embed :
await send ( embed = e )
else :
await send ( e )
except discord . Forbidden :
# send_to_owners suppresses Forbidden, logging it to console.
# As a result, this will only happen if a channel was set.
2024-01-06 13:54:31 -05:00
await self . bot . send_to_owners (
" [Update Checker] It appears that I am no longer allowed to send messages to the designated update channel. "
" From now on, it will DM you. "
)
2024-01-07 14:20:21 -05:00
if use_embed :
2024-01-07 14:27:19 -05:00
await self . bot . send_to_owners ( embed = e )
2024-01-07 14:20:21 -05:00
else :
2024-01-07 14:27:19 -05:00
await self . bot . send_to_owners ( e )
await self . conf . gochannel . set ( 0 )
# Was already inaccessible before I got here, so I'm just gonna leave it and look at it later -- Sea
# try:
# await channel.send(
# f"[Update Checker] Update found for repo: {repo.name}. Updating repos..."
# )
# except AttributeError:
# owner = (await self.bot.application_info()).owner
# await owner.send(
# "[Update Checker] It appears that the channel for this cog has been deleted. From now on, it will DM you."
# )
# channel = owner
# await self.conf.gochannel.set(0)
# except discord.errors.Forbidden:
# owner = (await self.bot.application_info()).owner
# await owner.send(
# "[Update Checker] It appears that I am no longer allowed to send messages to the designated update channel. From now on, it will DM you."
# )
# channel = owner
# await self.conf.gochannel.set(0)
# # Just a copy of `[p]cog update`, but without using ctx things
# try:
# installed_cogs = set(await cog.installed_cogs())
# updated = await cog._repo_manager.update_all_repos()
# updated_cogs = set(
# cog for repo in updated for cog in repo.available_cogs
# )
# installed_and_updated = updated_cogs & installed_cogs
# if installed_and_updated:
# await cog._reinstall_requirements(installed_and_updated)
# await cog._reinstall_cogs(installed_and_updated)
# await cog._reinstall_libraries(installed_and_updated)
# cognames = {c.name for c in installed_and_updated}
# message = humanize_list(tuple(map(inline, cognames)))
# except Exception as error:
# exception_log = (
# "Exception while updating repos in Update Checker \n"
# )
# exception_log += "".join(
# traceback.format_exception(
# type(error), error, error.__traceback__
# )
# )
# try:
# await channel.send(
# f"[Update Checker]: Error while updating repos.\n\n{exception_log}"
# )
# except discord.errors.Forbidden:
# pass
# else:
# try:
# await channel.send(
# f"[Update Checker]: Ran cog update. Updated cogs: {message}"
# )
# except discord.errors.Forbidden:
# pass
await asyncio . sleep ( 1 )
await self . conf . repos . set ( saving_dict )
2024-01-06 13:54:31 -05:00
async def fetch_feed ( self , url : str ) :
# Thank's to Sinbad's rss cog after which I copied this
timeout = aiohttp . client . ClientTimeout ( total = 15 )
try :
async with self . session . get ( url , timeout = timeout ) as response :
data = await response . read ( )
except ( aiohttp . ClientError , asyncio . TimeoutError ) :
return None
ret = feedparser . parse ( data )
if ret . bozo :
return None
return ret
2024-01-07 08:41:53 -05:00
async def fetch_gitea_thumbnail ( self , url : str ) - > str :
timeout = aiohttp . client . ClientTimeout ( total = 15 )
try :
async with self . session . get ( url , timeout = timeout ) as response :
2024-01-07 14:51:18 -05:00
data = await response . json ( )
2024-01-07 08:41:53 -05:00
except ( aiohttp . ClientError , asyncio . TimeoutError ) :
return None
return data [ ' avatar_url ' ]
2024-01-06 13:54:31 -05:00
@commands.is_owner ( )
@commands.group ( name = " cogupdater " , aliases = [ " cu " ] )
2024-01-07 14:51:18 -05:00
async def update ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Group command for controlling the update checker cog. """
@commands.is_owner ( )
@update.command ( )
2024-01-07 14:51:18 -05:00
async def auto ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Changes automatic cog updates to the opposite setting. """
2024-01-07 14:20:21 -05:00
# Was already inaccessible before I got here, so I'm just gonna leave it and look at it later -- Sea
# auto = await self.conf.auto()
# await self.conf.auto.set(not auto)
# status = "disabled" if auto else "enabled"
# await ctx.send(f"Auto cog updates are now {status}")
await ctx . send (
" This command is disabled for the time being. Cog updates will not run automatically, however notifications will still send. "
)
2024-01-06 13:54:31 -05:00
@commands.is_owner ( )
@update.command ( )
2024-01-07 14:51:18 -05:00
async def channel ( self , ctx : commands . Context , channel : discord . TextChannel = None ) :
2024-01-06 13:54:31 -05:00
"""
Sets a channel for update messages to go to .
If argument is not supplied , it will be sent to the default notifications channel ( s ) specified in ` [ p ] set ownernotifications ` .
By default , this goes to owner DMs .
"""
if channel :
await self . conf . gochannel . set ( channel . id )
await ctx . send ( f " Update messages will now be sent to { channel . mention } " )
else :
await self . conf . gochannel . set ( 0 )
await ctx . send ( " Update messages will now be DMed to you. " )
@commands.is_owner ( )
@update.command ( )
2024-01-07 14:51:18 -05:00
async def settings ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" See settings for the Update Checker cog.
Right now , this shows whether the bot updates cogs automatically and what channel logs are sent to .
"""
auto = await self . conf . auto ( )
channel = await self . conf . gochannel ( )
embed = await self . conf . embed ( )
if embed :
2024-01-08 01:06:30 -05:00
e = discord . Embed ( title = " Update Checker Settings " , color = 0x00FF00 )
2024-01-06 13:54:31 -05:00
e . add_field ( name = " Automatic Cog Updates " , value = str ( auto ) )
if channel == 0 :
channel = " Direct Messages "
else :
2024-01-07 14:27:19 -05:00
channel = self . bot . get_channel ( channel ) . name
if channel is None :
2024-01-06 13:54:31 -05:00
channel = " Unknown "
e . add_field ( name = " Update Channel " , value = channel )
await ctx . send ( embed = e )
else :
if channel == 0 :
channel = " Direct Messages "
else :
2024-01-07 14:27:19 -05:00
channel = self . bot . get_channel ( channel ) . name
if channel is None :
2024-01-06 13:54:31 -05:00
channel = " Unknown "
message = (
" ```css \n "
" [Update Checker settings] "
" ``````css \n "
f " [Automatic Cog Updates]: { str ( auto ) } \n "
f " [Update Channel]: { channel } "
" ``` "
)
await ctx . send ( message )
@commands.is_owner ( )
@update.command ( )
2024-01-07 14:51:18 -05:00
async def embed ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Toggles whether to use embeds or colorful codeblock messages when sending an update. """
c = await self . conf . embed ( )
await self . conf . embed . set ( not c )
word = " disabled " if c else " enabled "
await ctx . send ( f " Embeds are now { word } " )
@commands.is_owner ( )
@update.group ( name = " list " )
2024-01-07 14:51:18 -05:00
async def whiteblacklist ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Whitelist/blacklist certain repositories from which to receive updates. """
if ctx . invoked_subcommand is None :
data = await self . conf . all ( )
whitelist = data [ " whitelist " ]
blacklist = data [ " blacklist " ]
await ctx . send (
f " Whitelisted: { humanize_list ( tuple ( map ( inline , whitelist or [ ' None ' ] ) ) ) } \n Blacklisted: { humanize_list ( tuple ( map ( inline , blacklist or [ ' None ' ] ) ) ) } "
)
@whiteblacklist.group ( )
2024-01-07 14:51:18 -05:00
async def whitelist ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Whitelist certain repos from which to receive updates. """
@whitelist.command ( name = " add " )
2024-01-07 14:51:18 -05:00
async def whitelistadd ( self , ctx : commands . Context , * repos : Repo ) :
2024-01-06 13:54:31 -05:00
""" Add repos to the whitelist """
data = await self . conf . whitelist ( )
ds = set ( data )
2024-01-07 14:29:01 -05:00
ns = { r . name for r in repos }
2024-01-06 13:54:31 -05:00
ss = ds | ns
await self . conf . whitelist . set ( list ( ss ) )
await ctx . send ( f " Whitelist update successful: { humanize_list ( tuple ( map ( inline , ss ) ) ) } " )
@whitelist.command ( name = " remove " )
2024-01-07 14:51:18 -05:00
async def whitelistremove ( self , ctx : commands . Context , * repos : Repo ) :
2024-01-06 13:54:31 -05:00
""" Remove repos from the whitelist """
data = await self . conf . whitelist ( )
ds = set ( data )
2024-01-07 14:29:01 -05:00
ns = { r . name for r in repos }
2024-01-06 13:54:31 -05:00
ss = ds - ns
await self . conf . whitelist . set ( list ( ss ) )
await ctx . send (
f " Whitelist update successful: { humanize_list ( tuple ( map ( inline , ss or [ ' None ' ] ) ) ) } "
)
@whitelist.command ( name = " clear " )
2024-01-07 14:51:18 -05:00
async def whitelistclear ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Removes all repos from the whitelist """
await self . conf . whitelist . set ( [ ] )
await ctx . send ( " Whitelist update successful " )
@whiteblacklist.group ( )
2024-01-07 14:51:18 -05:00
async def blacklist ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Blacklist certain repos from which to receive updates. """
@blacklist.command ( name = " add " )
2024-01-07 14:51:18 -05:00
async def blacklistadd ( self , ctx : commands . Context , * repos : Repo ) :
2024-01-06 13:54:31 -05:00
""" Add repos to the blacklist """
data = await self . conf . blacklist ( )
ds = set ( data )
2024-01-07 14:29:01 -05:00
ns = { r . name for r in repos }
2024-01-06 13:54:31 -05:00
ss = ds | ns
await self . conf . blacklist . set ( list ( ss ) )
await ctx . send ( f " Backlist update successful: { humanize_list ( tuple ( map ( inline , ss ) ) ) } " )
@blacklist.command ( name = " remove " )
2024-01-07 14:51:18 -05:00
async def blacklistremove ( self , ctx : commands . Context , * repos : Repo ) :
2024-01-06 13:54:31 -05:00
""" Remove repos from the blacklist """
data = await self . conf . blacklist ( )
ds = set ( data )
2024-01-07 14:29:01 -05:00
ns = { r . name for r in repos }
2024-01-06 13:54:31 -05:00
ss = ds - ns
await self . conf . blacklist . set ( list ( ss ) )
await ctx . send (
f " Blacklist update successful: { humanize_list ( tuple ( map ( inline , ss or [ ' None ' ] ) ) ) } "
)
@blacklist.command ( name = " clear " )
2024-01-07 14:51:18 -05:00
async def blacklistclear ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Removes all repos from the blacklist """
await self . conf . blacklist . set ( [ ] )
await ctx . send ( " Blacklist update successful " )
@commands.is_owner ( )
@update.group ( name = " task " )
2024-01-07 14:51:18 -05:00
async def _group_update_task ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" View the status of the task (the one checking for updates). """
@_group_update_task.command ( )
2024-01-07 14:51:18 -05:00
async def status ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Get the current status of the update task. """
message = " Task is currently "
cancelled = self . task . cancelled ( )
if cancelled :
message + = " canceled. "
else :
done = self . task . done ( )
if done :
message + = " done. "
else :
message + = " running. "
try :
self . task . exception ( )
except asyncio . exceptions . InvalidStateError :
message + = " No error has been encountered. "
else :
2024-01-08 01:06:30 -05:00
message + = f " An error has been encountered. Please run ` { ctx . prefix } cogupdater task error` and report it to SeaswimmerTheFsh (seasw.) on the help server. "
2024-01-06 13:54:31 -05:00
await ctx . send ( message )
@_group_update_task.command ( )
2024-01-07 14:51:18 -05:00
async def error ( self , ctx : commands . Context ) :
2024-01-06 13:54:31 -05:00
""" Gets the latest error of the update task. """
try :
e = self . task . exception ( )
except asyncio . exceptions . InvalidStateError :
message = " No error has been encountered. "
else :
ex = traceback . format_exception ( type ( e ) , e , e . __traceback__ )
2024-01-07 14:27:19 -05:00
message = " An error has been encountered: " + box ( " " . join ( ex ) , " py " )
2024-01-06 13:54:31 -05:00
await ctx . send ( message )