2020-07-10 20:07:33 +00:00
import json , random , math , logging
from collections import defaultdict
2020-07-23 09:46:32 +00:00
from src . misc import logger
2020-07-11 15:54:08 +00:00
from src . config import settings
from src . database import db_cursor
2020-07-23 09:46:32 +00:00
from src . i18n import langs
2020-11-21 22:33:57 +00:00
from src . exceptions import EmbedListFull
2020-07-28 22:21:31 +00:00
from asyncio import TimeoutError
2020-11-28 13:08:37 +00:00
from math import ceil
2020-07-31 23:58:04 +00:00
2020-07-28 12:39:32 +00:00
import aiohttp
2020-07-10 20:07:33 +00:00
logger = logging . getLogger ( " rcgcdb.discord " )
# General functions
2020-07-23 09:46:32 +00:00
2020-08-01 20:28:08 +00:00
default_header = settings [ " header " ]
default_header [ ' Content-Type ' ] = ' application/json '
default_header [ " X-RateLimit-Precision " ] = " millisecond "
2020-07-23 09:46:32 +00:00
# User facing webhook functions
2020-07-27 03:16:50 +00:00
async def wiki_removal ( wiki_url , status ) :
for observer in db_cursor . execute ( ' SELECT webhook, lang FROM rcgcdw WHERE wiki = ? ' , ( wiki_url , ) ) :
2020-08-10 16:46:47 +00:00
_ = langs [ observer [ " lang " ] ] [ " discord " ] . gettext
2020-08-11 00:29:31 +00:00
reasons = { 410 : _ ( " wiki deleted " ) , 404 : _ ( " wiki deleted " ) , 401 : _ ( " wiki inaccessible " ) ,
2020-08-15 18:02:43 +00:00
402 : _ ( " wiki inaccessible " ) , 403 : _ ( " wiki inaccessible " ) , 1000 : _ ( " discussions disabled " ) }
2020-07-23 09:46:32 +00:00
reason = reasons . get ( status , _ ( " unknown error " ) )
2020-08-11 00:29:31 +00:00
await send_to_discord_webhook ( DiscordMessage ( " compact " , " webhook/remove " , webhook_url = [ ] , content = _ ( " This recent changes webhook has been removed for ` {reason} `! " ) . format ( reason = reason ) , wiki = None ) , webhook_url = observer [ " webhook " ] )
2020-07-26 08:00:27 +00:00
header = settings [ " header " ]
header [ ' Content-Type ' ] = ' application/json '
header [ ' X-Audit-Log-Reason ' ] = " Wiki becoming unavailable "
async with aiohttp . ClientSession ( headers = header , timeout = aiohttp . ClientTimeout ( 5.0 ) ) as session :
2020-07-27 12:13:36 +00:00
await session . delete ( " https://discord.com/api/webhooks/ " + observer [ " webhook " ] )
2020-07-26 08:00:27 +00:00
2020-07-23 09:46:32 +00:00
2020-08-11 12:11:47 +00:00
async def webhook_removal_monitor ( webhook_url : str , reason : int ) :
await send_to_discord_webhook_monitoring ( DiscordMessage ( " compact " , " webhook/remove " , None , content = " The webhook {} has been removed due to {} . " . format ( " https://discord.com/api/webhooks/ " + webhook_url , reason ) , wiki = None ) )
2020-07-23 09:46:32 +00:00
2020-07-23 19:12:07 +00:00
class DiscordMessage :
2020-07-10 20:07:33 +00:00
""" A class defining a typical Discord JSON representation of webhook payload. """
2020-07-23 09:46:32 +00:00
def __init__ ( self , message_type : str , event_type : str , webhook_url : list , wiki , content = None ) :
2020-07-10 20:07:33 +00:00
self . webhook_object = dict ( allowed_mentions = { " parse " : [ ] } )
self . webhook_url = webhook_url
2020-07-23 09:46:32 +00:00
self . wiki = wiki
2020-11-28 13:08:37 +00:00
self . length = 0
2020-07-10 20:07:33 +00:00
if message_type == " embed " :
2020-11-22 12:44:15 +00:00
self . _setup_embed ( )
2020-07-10 20:07:33 +00:00
elif message_type == " compact " :
2020-12-13 21:01:51 +00:00
if settings [ " event_appearance " ] . get ( event_type , { " emoji " : None } ) [ " emoji " ] :
content = settings [ " event_appearance " ] [ event_type ] [ " emoji " ] + " " + content
2020-07-10 20:07:33 +00:00
self . webhook_object [ " content " ] = content
2020-11-28 13:08:37 +00:00
self . length = len ( content )
2020-07-10 20:07:33 +00:00
self . event_type = event_type
2020-11-22 12:44:15 +00:00
def message_type ( self ) :
if " content " in self . webhook_object :
return " compact "
return " embed "
2020-07-10 20:07:33 +00:00
def __setitem__ ( self , key , value ) :
""" Set item is used only in embeds. """
try :
2020-11-28 13:08:37 +00:00
if key in ( ' title ' , ' description ' ) :
self . length + = len ( value ) - len ( self . embed . get ( key , " " ) )
2020-07-10 20:07:33 +00:00
self . embed [ key ] = value
except NameError :
raise TypeError ( " Tried to assign a value when message type is plain message! " )
def __getitem__ ( self , item ) :
return self . embed [ item ]
def __repr__ ( self ) :
""" Return the Discord webhook object ready to be sent """
return json . dumps ( self . webhook_object )
2020-11-22 12:44:15 +00:00
def _setup_embed ( self ) :
2020-11-21 22:33:57 +00:00
""" Setup another embed """
2020-07-10 20:07:33 +00:00
self . embed = defaultdict ( dict )
self . embed [ " color " ] = None
2020-11-28 13:08:37 +00:00
def __len__ ( self ) :
return self . length
2020-07-10 20:07:33 +00:00
def finish_embed ( self ) :
if self . embed [ " color " ] is None :
2020-12-06 13:39:29 +00:00
if settings [ " event_appearance " ] . get ( self . event_type , { " color " : None } ) [ " color " ] is None :
2020-07-23 09:46:32 +00:00
self . embed [ " color " ] = random . randrange ( 1 , 16777215 )
else :
2020-12-06 13:39:29 +00:00
self . embed [ " color " ] = settings [ " event_appearance " ] [ self . event_type ] [ " color " ]
2020-07-10 20:07:33 +00:00
else :
self . embed [ " color " ] = math . floor ( self . embed [ " color " ] )
2020-12-07 11:26:27 +00:00
if not self . embed [ " author " ] . get ( " icon_url " , None ) and settings [ " event_appearance " ] . get ( self . event_type , { " icon " : None } ) [ " icon " ] :
2020-12-06 13:39:29 +00:00
self . embed [ " author " ] [ " icon_url " ] = settings [ " event_appearance " ] [ self . event_type ] [ " icon " ]
2020-12-21 13:12:17 +00:00
self . finish_embed_message ( )
def finish_embed_message ( self ) :
2020-11-22 12:44:15 +00:00
if " embeds " not in self . webhook_object :
self . webhook_object [ " embeds " ] = [ self . embed ]
else :
2020-11-22 14:05:45 +00:00
if len ( self . webhook_object [ " embeds " ] ) > 9 :
2020-11-22 12:44:15 +00:00
raise EmbedListFull
self . webhook_object [ " embeds " ] . append ( self . embed )
2020-07-10 20:07:33 +00:00
2020-12-22 10:47:26 +00:00
def set_author ( self , name : str , url = " " , icon_url = " " ) :
2020-11-28 13:08:37 +00:00
self . length + = len ( name )
2020-07-10 20:07:33 +00:00
self . embed [ " author " ] [ " name " ] = name
self . embed [ " author " ] [ " url " ] = url
self . embed [ " author " ] [ " icon_url " ] = icon_url
2020-12-22 10:47:26 +00:00
def set_footer ( self , text : str , icon_url = " " ) :
self . length + = len ( text )
self . embed [ " footer " ] [ " text " ] = text
self . embed [ " footer " ] [ " icon_url " ] = icon_url
2020-07-10 20:07:33 +00:00
def add_field ( self , name , value , inline = False ) :
if " fields " not in self . embed :
self . embed [ " fields " ] = [ ]
2020-11-28 13:08:37 +00:00
self . length + = len ( name ) + len ( value )
2020-07-10 20:07:33 +00:00
self . embed [ " fields " ] . append ( dict ( name = name , value = value , inline = inline ) )
def set_avatar ( self , url ) :
self . webhook_object [ " avatar_url " ] = url
def set_name ( self , name ) :
self . webhook_object [ " username " ] = name
2020-11-28 13:08:37 +00:00
def stack_message_list ( messages : list ) - > list :
if len ( messages ) > 1 :
if messages [ 0 ] . message_type ( ) == " embed " :
# for i, msg in enumerate(messages):
# if not isinstance(msg, StackedDiscordMessage):
# break
# else: # all messages in messages are stacked, exit this if
# i += 1
removed_msgs = 0
2021-01-16 14:14:06 +00:00
# We split messages into groups of 10
2020-11-28 13:08:37 +00:00
for group_index in range ( ceil ( ( len ( messages ) ) / 10 ) ) :
2021-01-16 14:14:06 +00:00
message_group_index = group_index * 10 - removed_msgs # this helps us with calculations which messages we need
stackable = StackedDiscordMessage ( messages [ message_group_index ] ) # treat the first message from the group as main
for message in messages [ message_group_index + 1 : message_group_index + 10 ] : # we grab messages from messages list
2020-11-28 13:08:37 +00:00
try :
2021-01-16 14:14:06 +00:00
stackable . add_embed ( message ) # and to our main message we add ones after it that are from same group
except EmbedListFull : # if there are too many messages in our group we simply break so another group can be made
2020-11-28 13:08:37 +00:00
break
messages . remove ( message )
2021-01-16 14:14:06 +00:00
removed_msgs + = 1 # helps with calculating message_group_index
2020-11-28 13:08:37 +00:00
messages [ message_group_index ] = stackable
elif messages [ 0 ] . message_type ( ) == " compact " :
message_index = 0
2021-01-16 14:14:06 +00:00
while len ( messages ) > message_index + 1 : # as long as we have messages to stack
if ( len ( messages [ message_index ] ) + len ( messages [ message_index + 1 ] ) ) < 2000 : # if overall length is lower than 2000
2020-11-28 13:08:37 +00:00
messages [ message_index ] . webhook_object [ " content " ] = messages [ message_index ] . webhook_object [ " content " ] + " \n " + messages [ message_index + 1 ] . webhook_object [ " content " ]
messages [ message_index ] . length + = ( len ( messages [ message_index + 1 ] ) + 1 )
messages . remove ( messages [ message_index + 1 ] )
else :
message_index + = 1
return messages
2020-07-10 20:07:33 +00:00
2020-11-21 22:33:57 +00:00
class StackedDiscordMessage ( DiscordMessage ) :
def __init__ ( self , discordmessage : DiscordMessage ) :
if isinstance ( discordmessage , StackedDiscordMessage ) :
raise TypeError ( " Cannot transform StackedDiscordMessage " )
self . __dict__ = discordmessage . __dict__
2020-12-21 13:12:17 +00:00
self . event_type = " StackedDiscordMessage "
2020-11-21 22:33:57 +00:00
def stack ( self , messages : list ) :
for message in messages :
2020-12-22 10:47:26 +00:00
self . add_embed ( message )
2020-11-21 22:33:57 +00:00
2020-12-22 10:47:26 +00:00
def add_embed ( self , message ) :
if len ( self ) + len ( message ) > 6000 :
2020-11-28 13:08:37 +00:00
raise EmbedListFull
2020-12-22 10:47:26 +00:00
self . length + = len ( message )
2020-11-22 12:44:15 +00:00
self . _setup_embed ( )
2020-12-22 10:47:26 +00:00
self . embed = message . embed
2020-12-21 13:12:17 +00:00
self . finish_embed_message ( )
2020-11-21 22:33:57 +00:00
2020-07-10 20:07:33 +00:00
# Monitoring webhook functions
2020-07-27 03:16:50 +00:00
async def wiki_removal_monitor ( wiki_url , status ) :
await send_to_discord_webhook_monitoring ( DiscordMessage ( " compact " , " webhook/remove " , content = " Removing {} because {} . " . format ( wiki_url , status ) , webhook_url = [ None ] , wiki = None ) )
2020-07-23 09:46:32 +00:00
2020-08-06 13:26:06 +00:00
async def generic_msg_sender_exception_logger ( exception : str , title : str , * * kwargs ) :
""" Creates a Discord message reporting a crash """
2020-08-06 00:46:43 +00:00
message = DiscordMessage ( " embed " , " bot/exception " , [ None ] , wiki = None )
message [ " description " ] = exception
2020-08-06 13:26:06 +00:00
message [ " title " ] = title
2020-08-14 21:38:26 +00:00
for key , value in kwargs . items ( ) :
2020-08-06 13:26:06 +00:00
message . add_field ( key , value )
2020-08-01 10:45:41 +00:00
message . finish_embed ( )
await send_to_discord_webhook_monitoring ( message )
2020-07-26 08:00:27 +00:00
async def send_to_discord_webhook_monitoring ( data : DiscordMessage ) :
2020-07-23 09:46:32 +00:00
header = settings [ " header " ]
header [ ' Content-Type ' ] = ' application/json '
2020-07-26 08:00:27 +00:00
async with aiohttp . ClientSession ( headers = header , timeout = aiohttp . ClientTimeout ( 5.0 ) ) as session :
2020-07-23 09:46:32 +00:00
try :
2020-07-26 08:00:27 +00:00
result = await session . post ( " https://discord.com/api/webhooks/ " + settings [ " monitoring_webhook " ] , data = repr ( data ) )
2020-07-23 09:46:32 +00:00
except ( aiohttp . ClientConnectionError , aiohttp . ServerConnectionError ) :
logger . exception ( " Could not send the message to Discord " )
return 3
2020-07-26 08:00:27 +00:00
2020-08-01 01:02:58 +00:00
async def send_to_discord_webhook ( data : DiscordMessage , webhook_url : str ) - > tuple :
""" Sends a message to webhook
: return tuple ( status code for request , rate limit info ( None for can send more , string for amount of seconds to wait ) """
2020-08-01 20:28:08 +00:00
async with aiohttp . ClientSession ( headers = default_header , timeout = aiohttp . ClientTimeout ( 5.0 ) ) as session :
2020-07-27 16:32:30 +00:00
try :
result = await session . post ( " https://discord.com/api/webhooks/ " + webhook_url , data = repr ( data ) )
2020-08-01 14:57:34 +00:00
rate_limit = None if int ( result . headers . get ( ' x-ratelimit-remaining ' , " -1 " ) ) > 0 else result . headers . get ( ' x-ratelimit-reset-after ' , None )
2020-07-28 14:18:06 +00:00
except ( aiohttp . ClientConnectionError , aiohttp . ServerConnectionError , TimeoutError ) :
2020-07-27 16:32:30 +00:00
logger . exception ( " Could not send the message to Discord " )
2020-08-01 01:02:58 +00:00
return 3 , None
2020-08-11 12:11:47 +00:00
status = await handle_discord_http ( result . status , repr ( data ) , result , webhook_url )
2020-08-01 10:45:41 +00:00
if status == 5 :
return 5 , await result . json ( )
else :
return status , rate_limit
2020-07-23 09:46:32 +00:00
2020-08-11 12:11:47 +00:00
async def handle_discord_http ( code : int , formatted_embed : str , result : aiohttp . ClientResponse , webhook_url : str ) :
2020-07-23 09:46:32 +00:00
if 300 > code > 199 : # message went through
return 0
elif code == 400 : # HTTP BAD REQUEST result.status_code, data, result, header
logger . error (
" Following message has been rejected by Discord, please submit a bug on our bugtracker adding it: " )
logger . error ( formatted_embed )
2020-08-01 10:45:41 +00:00
logger . error ( await result . text ( ) )
2020-07-23 09:46:32 +00:00
return 1
elif code == 401 or code == 404 : # HTTP UNAUTHORIZED AND NOT FOUND
logger . error ( " Webhook URL is invalid or no longer in use, please replace it with proper one. " )
2020-08-11 12:11:47 +00:00
db_cursor . execute ( " DELETE FROM rcgcdw WHERE webhook = ? " , ( webhook_url , ) )
await webhook_removal_monitor ( webhook_url , code )
2020-07-23 09:46:32 +00:00
return 1
elif code == 429 :
logger . error ( " We are sending too many requests to the Discord, slowing down... " )
2020-08-01 10:45:41 +00:00
return 5
2020-07-23 09:46:32 +00:00
elif 499 < code < 600 :
logger . error (
" Discord have trouble processing the event, and because the HTTP code returned is {} it means we blame them. " . format (
code ) )
2020-07-29 16:33:40 +00:00
return 3
else :
return 4