├── .github └── workflows │ └── publish-ghcr.yaml ├── .gitignore ├── DefaultConstants.py ├── Dockerfile ├── GenerateDatabase.py ├── README.MD ├── checkers ├── Bongacams.py ├── Cam4.py ├── Chaturbate.py ├── Eplay.py ├── Fansly.py ├── Kick.py ├── Manyvids.py ├── Myfreecams.py ├── Onlyfans.py ├── Stripchat.py ├── Twitch.py └── Youtube.py ├── decorators ├── CommandLogger.py └── Permissions.py ├── docker-compose.yml ├── docker-entrypoint.sh ├── globals.py ├── images ├── avatars │ ├── calmStreamer.png │ └── pissedStreamer.png ├── errIcon.png ├── platformImages │ ├── CbImage.png │ ├── FansImage.png │ ├── KickImage.png │ ├── OFImage.png │ ├── bcImage.png │ ├── cam4Image.png │ ├── epImage.png │ ├── mfcImage.png │ ├── mvImage.png │ ├── scImage.png │ ├── twitchImage.png │ └── ytImage.png └── twitErrImg.jpg ├── plugins ├── checks.py ├── commands.py └── listeners.py ├── python.gitignore ├── requirements.txt ├── run.py └── utils ├── DataGrapher.py ├── Database.py ├── EmbedCreator.py ├── MiruViews.py ├── NoDriverBrowserCreator.py ├── Notifications.py ├── StaticMethods.py └── bot.py /.github/workflows/publish-ghcr.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image creator for sassbot experimental 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - flexiefae 8 | env: 9 | IMAGEVER: ${{ github.ref_name == 'master' && 'latest' || 'experimental' }} 10 | 11 | jobs: 12 | build-and-push: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out code 16 | uses: actions/checkout@v3 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | - name: Log in to GitHub Container Registry 20 | uses: docker/login-action@v1 21 | with: 22 | registry: ghcr.io 23 | username: bombg 24 | password: ${{ secrets.GH_PAT }} 25 | - name: Build and push 26 | uses: docker/build-push-action@v2 27 | with: 28 | context: . 29 | push: true 30 | tags: ghcr.io/bombg/sassbot:${{env.IMAGEVER}} 31 | platforms: linux/arm64, linux/amd64 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secrets/token 2 | .env/ 3 | __pycache__ 4 | graphs 5 | cassbotDbDownloadScript.txt 6 | CassDBAutoDL.lnk 7 | commandLogs.txt 8 | deletedMessageLogs.txt 9 | Fansscreenshot.png 10 | KickScreenshot.png 11 | sassBot.db 12 | test.py 13 | TODO.txt 14 | images/fansIcon.jpg 15 | .vscode 16 | .venv 17 | Fansscreenshot.jpg 18 | KickScreenshot.jpg 19 | ofScreenshot.jpg 20 | NoDriverTest.py 21 | AppConstants.py 22 | tests 23 | .dockerignore -------------------------------------------------------------------------------- /DefaultConstants.py: -------------------------------------------------------------------------------- 1 | class Constants: 2 | # COPY THIS FILE AND CREATE YOUR OWN VERSION NAMED AppConstants.py THAT WAY YOU CAN PULL CHANGES WITHOUT OVERWRITING SETTINGS 3 | TEST_SERVER = False 4 | SASSBOT_LOG_LEVEL = 20 # DEBUG = 10, INFO = 20, WARNING = 30, ERROR = 40, CRITICAL = 50 5 | OTHER_LIBRARIES_LOG_LEVEL = 20 # DEBUG = 10, INFO = 20, WARNING = 30, ERROR = 40, CRITICAL = 50 6 | if TEST_SERVER: 7 | GUILD_ID =313876691082674178 #Guild ID of the discord server 8 | whiteListedRoleIDs = [145802742647095296] # IDs of Roles you wish to be white listed for some commands. You can also add user IDs if you want to add an individual without a role 9 | MOD_ROLE_ID = 1096930045685145710 # Used to ping mods to take action on an approved ban appeal 10 | # Channel ID the bot will post notifications to 11 | KICK_NOTIFICATION_CHANNEL_ID = 1137599805787480214 12 | CB_NOTIFICATION_CHANNEL_ID = 1137599805787480214 13 | FANS_NOTIFICATION_CHANNEL_ID = 1137599805787480214 14 | OF_NOTIFICATION_CHANNEL_ID = 1137599805787480214 15 | YT_NOTIFICATION_CHANNEL_ID = 1137599805787480214 16 | TWITCH_NOTIFICATION_CHANNEL_ID = 1137599805787480214 17 | CAM4_NOTIFICATION_CHANNEL_ID = 1137599805787480214 18 | MFC_NOTIFICATION_CHANNEL_ID = 1137599805787480214 19 | BC_NOTIFICATION_CHANNEL_ID = 1137599805787480214 20 | SC_NOTIFICATION_CHANNEL_ID = 1137599805787480214 21 | EP_NOTIFICATION_CHANNEL_ID = 1137599805787480214 22 | MV_NOTIFICATION_CHANNEL_ID = 1137599805787480214 23 | CONFESSTION_CHANNEL_ID = 1137599805787480214 24 | APPEAL_CHANNEL_ID = 1137599805787480214 25 | 26 | CONFESSION_COMMAND_ID = 1159423004346957835 27 | CONFESS_REVIEW_COMMAND_ID = 1159423004346957834 28 | APPEAL_COMMAND_ID = 1159321755270250571 29 | APPEAL_REVIEW_COMMAND_ID = 1159321755270250570 30 | 31 | # Leave an empty string if you don't wish to use a proxy for a checker. 32 | # Kick/OF/Fansly use nodriver, which doesn't support authenticated proxies 33 | # All the other platforms assumes you're using a socks5 proxy, so you can leave out the socks5:// part 34 | # Commented out platforms don't support proxies 35 | KICK_PROXY = "" # Chrome/Chromium doesn't support authenticated proxies 36 | FANS_PROXY = "127.0.0.1:8888" # Chrome/Chromium doesn't support authenticated proxies 37 | OF_PROXY = "" # Chrome/Chromium doesn't support authenticated proxies 38 | #CB_PROXY = "" # Everything below assumed is a socks5 proxy. 39 | MV_PROXY = "" 40 | BC_PROXY = "" 41 | SC_PROXY = "" 42 | EP_PROXY = "" 43 | CAM4_PROXY = "" 44 | MFC_PROXY = "" 45 | #YT_PROXY = "" 46 | #TWITCH_PROXY = "" 47 | else: 48 | GUILD_ID =1058859922219081778 #Guild ID of the discord server 49 | whiteListedRoleIDs = [1062179283705020486,145802742647095296,1100148453792813086,245364417783398400] # IDs of Roles you wish to be white listed for some commands. You can also add user IDs if you want to add an individual without a role 50 | MOD_ROLE_ID = 1096930045685145710 # Used to ping mods to take action on an approved ban appeal 51 | # Channel ID the bot will post notifications to 52 | KICK_NOTIFICATION_CHANNEL_ID = 1268796965743886448 53 | CB_NOTIFICATION_CHANNEL_ID = 1268796965743886448 54 | FANS_NOTIFICATION_CHANNEL_ID = 1268796965743886448 55 | OF_NOTIFICATION_CHANNEL_ID = 1061931918414852146 56 | YT_NOTIFICATION_CHANNEL_ID = 1268796965743886448 57 | TWITCH_NOTIFICATION_CHANNEL_ID = 1268796965743886448 58 | CAM4_NOTIFICATION_CHANNEL_ID = 1268796965743886448 59 | MFC_NOTIFICATION_CHANNEL_ID = 1268796965743886448 60 | BC_NOTIFICATION_CHANNEL_ID = 1268796965743886448 61 | SC_NOTIFICATION_CHANNEL_ID = 1268796965743886448 62 | EP_NOTIFICATION_CHANNEL_ID = 1268796965743886448 63 | MV_NOTIFICATION_CHANNEL_ID = 1268796965743886448 64 | CONFESSTION_CHANNEL_ID = 1158240422997528637 65 | APPEAL_CHANNEL_ID = 1158240422997528637 66 | 67 | CONFESSION_COMMAND_ID = 1159321755270250571 68 | CONFESS_REVIEW_COMMAND_ID = 1159321755270250570 69 | APPEAL_COMMAND_ID = 1159321755270250571 70 | APPEAL_REVIEW_COMMAND_ID = 1159321755270250570 71 | 72 | # Leave an empty string if you don't wish to use a proxy for a checker. 73 | # Kick/OF/Fansly use nodriver, which doesn't support authenticated proxies 74 | # All the other platforms assumes you're using a socks5 proxy, so you can leave out the socks5:// part 75 | # Commented out platforms don't support proxies 76 | KICK_PROXY = "" # Chrome/Chromium doesn't support authenticated proxies 77 | FANS_PROXY = "127.0.0.1:8888" # Chrome/Chromium doesn't support authenticated proxies 78 | OF_PROXY = ""# Chrome/Chromium doesn't support authenticated proxies 79 | #CB_PROXY = "" # Everything below assumed is a socks5 proxy. 80 | MV_PROXY = "" 81 | BC_PROXY = "" 82 | SC_PROXY = "" 83 | EP_PROXY = "" 84 | CAM4_PROXY = "" 85 | MFC_PROXY = "" 86 | #YT_PROXY = "" 87 | #TWITCH_PROXY = "" 88 | 89 | WAIT_BETWEEN_MESSAGES = 1800 # minimum amount of time in seconds the stream has to be offline before new notification messages. 90 | MIN_TIME_BEFORE_AVATAR_CHANGE = 48 # Minimum time before avatar changes -- in hours 91 | ONLINE_MESSAGE_REBROADCAST_TIME = 86400 #Time in seconds the stream will be online before another online notification will be broadcasted 92 | TIME_BEFORE_BOT_RESTART = 86400 #time in seconds before bot will restart. Restart checks are made every 10 minutes 93 | TIME_OFFLINE_BEFORE_RESTART = 900 #minimum time in seconds stream needs to be offline before bot will restart IF TIME_BEFORE_BOT_RESTART time has been met 94 | TEMP_TITLE_UPTIME = 57600 #Time in seconds temp titles will be used before default titles are used 95 | TIME_BEFORE_REVIEW_RESET = 300 # Time a whitelisted person has to review a confession before its added back to the queue 96 | 97 | # Nodriver default for retries is 4, but for slow machines this could require a lot more (raspberry pi 3b+ tested with 20 and still fails occasionally) 98 | NODRIVER_BROWSER_CONNECT_RETRIES = 25 99 | NODRIVER_WAIT_MULTIPLIER = 8 # multiplier for nodriver waits. Make this longer for slower machines 100 | 101 | # Platform Check Timers - all in seconds 102 | KICK_CHECK_TIMER = 180 103 | CB_CHECK_TIMER = 180 104 | FANS_CHECK_TIMER = 220 105 | OF_CHECK_TIMER = 170 106 | YT_CHECK_TIMER = 180 107 | TWITCH_CHECK_TIMER = 180 108 | CAM4_CHECK_TIMER = 1800 # Using very long Cam4 check timer to be on safe side. Lower at your own risk. Still unsure if safe. 109 | MFC_CHECK_TIMER = 180 110 | BC_CHECK_TIMER = 180 111 | SC_CHECK_TIMER = 180 112 | EP_CHECK_TIMER = 180 113 | MV_CHECK_TIMER = 180 114 | 115 | AVATAR_CHECK_TIMER = 130 # Timer for checking last online time before changing between happy/angry avatars 116 | STATUS_CHECK_TIMER = 125 # Timer for checking online status and changing the bot status. Also used for record keeping 117 | CONFESSION_CHECK_TIMER = 20 # How often new confessions are checked 118 | APPEAL_CHECK_TIMER = 20 # How often new appeals are checked 119 | 120 | CONFESSION_ALERT_INTERVALS = [0,0,1800,7200,18000,43200] # Seconds between unreveiwed confession alerts. Starts at index 1. 1st alert 0 seconds, 2nd alert 1800 etc. New confessions reset count 121 | APPEAL_ALERT_INTERVALS = [0,0,1800,7200,18000,43200] # Seconds between unreveiwed appeal alerts. Starts at index 1. 1st alert 0 seconds, 2nd alert 1800 etc. New appeals reset count 122 | 123 | SMART_ALERT_LOOK_AHEAD = 3 #number of hours smart alert looks ahead to make sure conditions are still met (to make sure alerts aren't made too late into a stream) 124 | PERCENTAGE_OF_MAX = 0.85 # Percent of maximum users online before a smart alert goes off 125 | SECONDS_BETWEEN_SMART_ALERTS = 21600 # minimum number of seconds before another smart alert goes off 126 | 127 | RECORD_KEEPING_START_DATE = 1694340841 #Epoch time in seconds when you started using this bot Use: https://www.epochconverter.com/ 128 | 129 | PIN_TIME_LONG = 4 # number in hours. 130 | PIN_TIME_SHORT = 1 # same as above but this is used for images added via rebroadcast-image command 131 | 132 | # For role pings to work you will first need to turn them on via the /ping-toggle True/False command. 133 | # if you don't want a specific platform to get a ping, just leave an empty string 134 | # If you wish to ping everyone simply input @everyone, but if you wish to ping a specific role you'll need to get the role ID and assemble it like so <@&putRoleIDHere> 135 | # for example if the role id is 999 then you'd put ROLES_TO_PING = '<@&999> ' 136 | # If you want to ping multiple roles then just put them in the same string. i.e. ROLES_TO_PING = '<@&999> @everyone ' 137 | # Leave a space at the end of the string i.e ROLES_TO_PING = '@everyone ' 138 | KICK_ROLES_TO_PING = "@everyone " 139 | CB_ROLES_TO_PING = "@everyone " 140 | FANS_ROLES_TO_PING = "@everyone " 141 | OF_ROLES_TO_PING = "@everyone " 142 | YT_ROLES_TO_PING = "@everyone " 143 | TWITCH_ROLES_TO_PING = "@everyone " 144 | CAM4_ROLES_TO_PING = "@everyone " 145 | MFC_ROLES_TO_PING = "@everyone " 146 | BC_ROLES_TO_PING = "@everyone " 147 | SC_ROLES_TO_PING = "@everyone " 148 | EP_ROLES_TO_PING = "@everyone " 149 | MV_ROLES_TO_PING = "@everyone " 150 | 151 | # For rerun announcements/pings to work you will first need to turn them on via the /announce-rerun-toggle True/False command AND the /ping-toggle True/False command. 152 | # if you don't want a specific platform to get a rerun ping, just leave an empty string (It will still get announced if turned on) 153 | # If you wish to ping everyone simply input @everyone, but if you wish to ping a specific role you'll need to get the role ID and assemble it like so <@&putRoleIDHere> 154 | # for example if the role id is 999 then you'd put ROLES_TO_PING = '<@&999> ' 155 | # If you want to ping multiple roles then just put them in the same string. i.e. ROLES_TO_PING = '<@&999> @everyone ' 156 | # Leave a space at the end of the string i.e ROLES_TO_PING = '@everyone ' 157 | KICK_RERUN_ROLES_TO_PING = "" 158 | CB_RERUN_ROLES_TO_PING = "" 159 | FANS_RERUN_ROLES_TO_PING = "" 160 | OF_RERUN_ROLES_TO_PING = "" 161 | YT_RERUN_ROLES_TO_PING = "" 162 | TWITCH_RERUN_ROLES_TO_PING = "" 163 | CAM4_RERUN_ROLES_TO_PING = "" 164 | MFC_RERUN_ROLES_TO_PING = "" 165 | BC_RERUN_ROLES_TO_PING = "" 166 | SC_RERUN_ROLES_TO_PING = "" 167 | EP_RERUN_ROLES_TO_PING = "" 168 | MV_RERUN_ROLES_TO_PING = "" 169 | 170 | #Generic name of the streamer that will be used for all notifications 171 | streamerName = "LitneySpears" 172 | 173 | #Usernames associated with each platform - if not applicable leave an empty array. i.e. cbUserName = [] 174 | #If the streamer has multiple accounts for a paltform, add an extra username to the array i.e. cbUserName = ['user1','user2'] 175 | kickUserName = ['LitneySpears'] 176 | cbUserName = [] 177 | fansUserName = ['Litneyspearsx'] 178 | ofUserName = ['litneyspearsx','litneyspearsfree'] 179 | ytUserName = ['litneyspears_'] 180 | twitchUserName = ['litneyspears_'] 181 | cam4UserName = [] 182 | mfcUserName = [] 183 | bcUserName = [] 184 | scUserName = [] 185 | epUserName = [] 186 | mvUserName = [] #case sensitive if you want the the avatar to be pulled 187 | 188 | twitchUrl = f"https://www.twitch.tv/litneyspears_" #Add a valid twitch URL here even if you streamer doesn't have twitch or else the presence won't update properly 189 | 190 | #affiliate api link to see online users in cb https://chaturbate.com/affiliates/promotools/api_usersonline/ 191 | # This makes assumptions thaty may not be true for your model, so go to the link above and make an API url for yourself. 192 | # I've found this responds more reliably when you narrow down the search more. So add region, and any tags your model always uses 193 | cbJsonLimit = 500 # 500 is max. 100 is default if you remove the limit tag. Keep the limit tag in the api url OR change this to 100 if the tag is removed 194 | cbApiUrl = f"https://chaturbate.com/api/public/affiliates/onlinerooms/?wm=3pmuc&client_ip=request_ip&gender=f®ion=northamerica&limit={cbJsonLimit}" 195 | 196 | # Colors the line that runs vertically on the left side of the embed. Also used to color graphs 197 | kickEmbedColor = "#52fb19" 198 | fansEmbedColor = "#a0816c" 199 | ofEmbedColor = "#018ccf" 200 | cbEmbedColor = "#f6922f" 201 | ytEmbedColor = "#ff0000" 202 | twitchEmbedColor = "#9146FF" 203 | cam4EmbedColor = "#dd5d2c" 204 | mfcEmbedColor = "#377c1d" 205 | bcEmbedColor = "#97323a" 206 | scEmbedColor = "#a02831" 207 | epEmbedColor = "#f03d4c" 208 | mvEmbedColor = "#722a9e" 209 | 210 | # Mainly Used in stream-status command 211 | linkTreeUrl = "https://allmylinks.com/litneyspears" 212 | 213 | # Titles for announcement embeds 214 | # Titles for platforms that have optional titles or no titles at all 215 | # You can also use the /title command to create temporary titles for a platform on the fly, but once the TEMP_TITLE_UPTIME time is up then it defaults back to these titles 216 | # If the titles contain any variation of RR/Rerun/not live then it will be detected by rerun detection 217 | fansDefaultTitle = "🍑💦Now taking good vibes on stream! =)🍑💦" 218 | ofDefaultTitle = "Naughty time? =)" 219 | cam4DefaultTitle = "Cam4 Naughty Time." 220 | mfcDefaultTitle = "MFC Fun Time." 221 | bcDefaultTitle = "BongaCams Fun Time." 222 | scDefaultTitle = "StripChat Fun Time." 223 | epDefaultTitle = "ePlay Fun Time." 224 | mvDefaultTitle = "Manyvids Fun Time Starts Now!" 225 | 226 | # This is the text that will appear above the embed. Role mentions will be added before this text, and a link to the stream will be added after 227 | # i.e. @everyone https://kick.com/StreamerName 228 | kickAboveEmbedText = f"{streamerName} is live on Kick!" 229 | fansAboveEmbedText = f"{streamerName} is live on Fansly!" 230 | ofAboveEmbedText = f"{streamerName} is live on Onlyfans!" 231 | cbAboveEmbedText = f"{streamerName} is live on Chaturbate!" 232 | ytAboveEmbedText = f"{streamerName} is live on YouTube!" 233 | twitchAboveEmbedText = f"{streamerName} is live on Twitch!" 234 | cam4AboveEmbedText = f"{streamerName} is live on Cam4!" 235 | mfcAboveEmbedText = f"{streamerName} is live on MyFreeCams!" 236 | bcAboveEmbedText = f"{streamerName} is live on BongaCams!" 237 | scAboveEmbedText = f"{streamerName} is live on StripChat!" 238 | epAboveEmbedText = f"{streamerName} is live on ePlay!" 239 | mvAboveEmbedText = f"{streamerName} is live on ManyVids!" 240 | 241 | # This is small text that will appear below the title, and above the main image inside the embed. 242 | kickBelowTitleText = f"{streamerName} is now live on Kick!" 243 | fansBelowTitleText = f"{streamerName} is now live on Fansly!" 244 | ofBelowTitleText = f"{streamerName} is now live on Onlyfans!" 245 | cbBelowTitleText = f"{streamerName} is now live on Chaturbate!" 246 | ytBelowTitleText = f"{streamerName} is now live on YouTube!" 247 | twitchBelowTitleText = f"{streamerName} is now live on Twitch!" 248 | cam4BelowTitleText = f"{streamerName} is now live on Cam4!" 249 | mfcBelowTitleText = f"{streamerName} is now live on MyFreeCams!" 250 | bcBelowTitleText = f"{streamerName} is now live on BongaCams!" 251 | scBelowTitleText = f"{streamerName} is now live on StripChat!" 252 | epBelowTitleText = f"{streamerName} is now live on ePlay!" 253 | mvBelowTitleText = f"{streamerName} is now live on ManyVids!" 254 | 255 | # Leave empty strings if you want to use default thumbnail behavior; which is: (1)pull thumbnail from platform, (2)if it doesn't exist use image from image list, (3)if list empty use defaultThumbnail 256 | # Add your own image path/url if you want to exclusively use the same image over and over for a specific platform's thumbnail 257 | # If you want thumbnails to only come from the image list, make the string equal to LIST i.e cbThumbnail = "LIST" . Useful if you don't want NSFW thumbnails in alerts 258 | kickThumbnail = "" 259 | fansThumbnail = "" 260 | ofThumbnail = "" 261 | cbThumbnail = "LIST" 262 | ytThumbnail = "" 263 | twitchThumbnail = "" 264 | cam4Thumbnail = "" 265 | mfcThumbnail = "" 266 | bcThumbnail = "" 267 | scThumbnail = "" 268 | epThumbnail = "" 269 | mvThumbnail = "" 270 | 271 | # Icon in this case is the small image that shows in the top left of the imbed before the streamer's name for that platform 272 | # This is used if an avatar/icon can't be found on a platform, otherwise the platform's version will be used 273 | defaultIcon = 'images/errIcon.png' 274 | 275 | # This thumbnail is used if there is no thumbnail for a platform AND there is nothing in the image list 276 | defaultThumbnail = 'images/twitErrImg.jpg' 277 | 278 | # Calm is default bot avatar, pissed is what it changes to after MIN_TIME_BEFORE_AVATAR_CHANGE has been met 279 | # Make them the same image if you don't want the feature to change anything 280 | calmAvatar = 'images/avatars/calmStreamer.png' 281 | pissedAvatar = 'images/avatars/pissedStreamer.png' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.3-slim AS build 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | # Build dummy packages to skip installing them and their dependencies -- Copied from FlareSolverr 5 | RUN apt update -y && apt install -y git \ 6 | && apt-get install -y --no-install-recommends equivs \ 7 | && equivs-control libgl1-mesa-dri \ 8 | && printf 'Section: misc\nPriority: optional\nStandards-Version: 3.9.2\nPackage: libgl1-mesa-dri\nVersion: 99.0.0\nDescription: Dummy package for libgl1-mesa-dri\n' >> libgl1-mesa-dri \ 9 | && equivs-build libgl1-mesa-dri \ 10 | && mv libgl1-mesa-dri_*.deb /libgl1-mesa-dri.deb \ 11 | && equivs-control adwaita-icon-theme \ 12 | && printf 'Section: misc\nPriority: optional\nStandards-Version: 3.9.2\nPackage: adwaita-icon-theme\nVersion: 99.0.0\nDescription: Dummy package for adwaita-icon-theme\n' >> adwaita-icon-theme \ 13 | && equivs-build adwaita-icon-theme \ 14 | && mv adwaita-icon-theme_*.deb /adwaita-icon-theme.deb 15 | 16 | RUN python3 -m venv /venv 17 | ENV PATH=/venv/bin:$PATH 18 | 19 | COPY requirements.txt . 20 | RUN pip install -r requirements.txt && \ 21 | pip install uvloop 22 | 23 | # Buidling final image, moving over venv 24 | FROM python:3.11.3-slim 25 | ENV DEBIAN_FRONTEND=noninteractive 26 | 27 | WORKDIR /opt/SassBot 28 | 29 | RUN apt update -y && apt install -y --no-install-recommends chromium xvfb \ 30 | # Remove temporary files and hardware decoding libraries -- Copied from FlareSolverr 31 | && rm -rf /var/lib/apt/lists/* \ 32 | && rm -f /usr/lib/x86_64-linux-gnu/libmfxhw* \ 33 | && rm -f /usr/lib/x86_64-linux-gnu/mfx/* 34 | 35 | COPY --from=build /venv /venv 36 | ENV PATH=/venv/bin:$PATH 37 | COPY . . 38 | 39 | RUN chmod +x docker-entrypoint.sh 40 | ENTRYPOINT ["/bin/sh", "-c", "./docker-entrypoint.sh"] -------------------------------------------------------------------------------- /GenerateDatabase.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import time 3 | #INTEGER = int 4 | #REAL = float 5 | #TEXT = string 6 | #BLOB = bytes 7 | #NULL = none 8 | conn = sqlite3.connect("sassBot.db") 9 | 10 | cur = conn.cursor() 11 | 12 | cur.execute('''CREATE TABLE IF NOT EXISTS platforms 13 | ( 14 | platform_name TEXT PRIMARY KEY, 15 | last_online_message REAL, 16 | last_stream_start_time REAL, 17 | last_stream_end_time REAL, 18 | rerun_playing INTEGER 19 | ) 20 | ''') 21 | 22 | cur.execute('''CREATE TABLE IF NOT EXISTS platform_accounts 23 | ( 24 | account_name TEXT NOT NULL, 25 | platform_name TEXT NOT NULL, 26 | last_online_message REAL, 27 | last_stream_start_time REAL, 28 | last_stream_end_time REAL, 29 | temp_title TEXT, 30 | temp_title_time REAL, 31 | PRIMARY KEY (account_name, platform_name), 32 | FOREIGN KEY(platform_name) REFERENCES platforms(platform_name) 33 | ) 34 | ''') 35 | 36 | cur.execute('''CREATE TABLE IF NOT EXISTS subathon 37 | ( 38 | subathon INTEGER, 39 | start_time REAL, 40 | end_time REAL, 41 | longest_subathon INTEGER, 42 | longest_subathon_time REAL 43 | ) 44 | ''') 45 | 46 | cur.execute('''CREATE TABLE IF NOT EXISTS stream 47 | ( 48 | last_online REAL, 49 | last_offline REAL, 50 | last_stream_length REAL, 51 | tw_img_list TEXT, 52 | tw_img_queue TEXT, 53 | img_pin INTEGER, 54 | img_pin_url TEXT, 55 | img_banned_list TEXT, 56 | everyone_ping INTEGER, 57 | rerun_ping INTEGER 58 | ) 59 | ''') 60 | 61 | cur.execute('''CREATE TABLE IF NOT EXISTS user_presence_stats 62 | ( 63 | date TEXT PRIMARY KEY, 64 | week_day INTEGER, 65 | user_presences TEXT 66 | ) 67 | ''') 68 | 69 | cur.execute('''CREATE TABLE IF NOT EXISTS confessions 70 | ( 71 | confession_id INTEGER PRIMARY KEY, 72 | confession TEXT, 73 | confession_title TEXT, 74 | review_status INTEGER, 75 | reviewer_id INTEGER, 76 | reviewer_name TEXT, 77 | date_added INTEGER, 78 | date_reviewed INTEGER 79 | ) 80 | ''') 81 | 82 | cur.execute('''CREATE TABLE IF NOT EXISTS appeals 83 | ( 84 | appeal_id INTEGER PRIMARY KEY, 85 | appeal TEXT, 86 | appeal_title TEXT, 87 | appeal_status INTEGER, 88 | appealer_id INTEGER, 89 | appealer_name TEXT, 90 | reviewer_id INTEGER, 91 | reviewer_name TEXT, 92 | date_added INTEGER, 93 | date_reviewed INTEGER 94 | ) 95 | ''') 96 | 97 | platform_list =[ 98 | ("chaturbate",0,0,0), 99 | ("onlyfans",0,0,0), 100 | ("fansly",0,0,0), 101 | ("twitch",0,0,0), 102 | ("youtube",0,0,0), 103 | ("kick",0,0,0), 104 | ("cam4",0,0,0), 105 | ("mfc",0,0,0), 106 | ("bongacams",0,0,0), 107 | ("stripchat",0,0,0), 108 | ("eplay",0,0,0), 109 | ("manyvids",0,0,0) 110 | ] 111 | subathon_values =[ 112 | (0,0,0,0,None) 113 | ] 114 | stream_values = [ 115 | (0,0,0) 116 | ] 117 | cur.executemany(''' 118 | INSERT INTO platforms (platform_name,last_online_message,last_stream_start_time,last_stream_end_time) VALUES (?,?,?,?) 119 | ''', platform_list) 120 | cur.executemany(''' 121 | INSERT INTO subathon (subathon, start_time, end_time, longest_subathon, longest_subathon_time) VALUES (?,?,?,?,?) 122 | ''', subathon_values) 123 | cur.executemany(''' 124 | INSERT INTO stream (last_online, last_offline, last_stream_length) VALUES (?,?,?) 125 | ''', stream_values) 126 | conn.commit() 127 | 128 | cur.close() 129 | conn.close() -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # SassBot 2 | ## Discord bot for your favorite e-girl 3 | ### Platforms Supported: 4 | - Twitch 5 | - Kick 6 | - Youtube 7 | - Onlyfans 8 | - Fansly 9 | - Chaturbate 10 | - Cam4 11 | - MyFreeCams 12 | - BongaCams 13 | - StripChat 14 | - ePlay 15 | - ManyVids 16 | ### Features 17 | - Get notifications when your streamer is online sent to a discord channel 18 | - Rerun detection/support. If stream titles contains a variation of rerun/rr/not live then it will be detected as a rerun. 19 | - by default reruns won't be announced, but you can choose to announce them to custom roles just for reruns 20 | - Track multiple accounts per platform. (for example, if they have a paid and free account) 21 | - There's a minimum time between notifications, so your discord members won't get spammed if you're having connection issues. 22 | - Bot's status reflects what the streamer is doing. Offline or streaming on Kick, yt, etc. 23 | - White list commands to certain roles. 24 | - Log who uses what commands 25 | - Add/remove images yourself to embed into notifications via image commands. Images are rotated through, so add as many as you like. 26 | - Rebroadcast online notifications with rebroadcast commands 27 | - Subathon online timers via subathon commands 28 | - Send alerts during peak discord usage times 29 | - ... and many more! 30 | ### Commands 31 | - /stream-status - See if your streamer is currently offline or online. Shows time online/offline and total time streamed. 32 | - /ping-toggle - Toggle for role pings in online announcements. This is defaulted to false, so set true if you want role pings 33 | - /announce-rerun-toggle - Toggle if reruns will be announced. If a title contains a variation of rr/rerun/not live then the stream will be detected as a rerun. 34 | - /title - Allows you to temporarily add a custom title for a platform/username combo. TEMP_TITLE_UPTIME in constants.py determines how long a temp title added via /title will be used for. 35 | - Image list commands - Images can be manually added. These images are used for embeds for all supported platforms. 36 | - /image-list-show - show the list of image urls that will be rotated through during online announcements 37 | - /image-list-add - add an image url to the image list 38 | - /image-list-remove - remove an image from the image list. Must provide the url 39 | - Pin image commands - If there is no pinned image, Kick, Twitch, CB, and YT use thumbnail images from the video feed, OF/Fans will embed images from the image list. 40 | - /image-pin - pin an image for a number of hours you choose. Pinned images will be embedded into notifications in the announcements channel. 41 | - /image-check-pin - Check to see if an image is pinned 42 | - /image-unpin - unpin an image if it is pinned. 43 | - /rebroadcast - Send out online notifications to the announcements channel IF the streamer is online. Isn't isntant but goes off when the next online check is made. 44 | - /rebroadcast-image - same as above but you can attach a URL of an image to embed into the new announcement(s) 45 | - Subathon commands - apart from commands, subathon status is also displayed in the Bot's status. 46 | - /subathon - check the status of the current subathon, if there is one. 47 | - /subathon-start - start a subathon timer 48 | - /subathon-end - end a subathon timer 49 | - Stats 50 | - /users-graph Get a graph of online/dnd/idle/ users in your discord. The data is mainly used to send out alerts but why not use it for graphs as well! 51 | - Has two optional arguements, InputDate and Days. InputDate defaults to todays date and days (to include in graph) defaults to 1. 52 | - /stream-stats Shows total time streamed for the week, two weeks, and four weeks for each platform. 53 | - Anonymous questions/confessions 54 | - /confess - unlogged and anonymous confession that a user can post to a channel via the bot. Must be reviewed first 55 | - /confess-review - whitelisted roles/people can review submitted confessions and approve or deny them. 56 | - approved confessions/questions are posted to a preset channel immediately 57 | - Test commands 58 | - /test-permission - test if Sassbot has the necessary permissions to post in the specified channel ID. 59 | 60 | ### How to set up the bot: 61 | 62 | 1. Go to https://discord.com/developers and set up an app and get a token. Put that token in a file called token (no file extension needed), and put that file in a subfolder called secrets. 63 | 2. At https://discord.com/developers/ - generate an invite code under oauth2 to invite your bot to your server. Under scope make sure bot is checked. 64 | Give the bot permissions you want it to have. At minimum it'll need to be able to post messages and embeds 65 | 3. Go to the bot section in the developers portal and turn on all the intents. 66 | 4. In the Constants.py file, change the GUILD_ID value to the guild id of your server, and each platform NOTIFICATION_CHANNEL_ID to the channel ID you want alerts posted to. 67 | To easily get both of these IDs, in discord advanced options turn on developer mode. After that you can right click servers/channels and a new option to copy the IDs will be available. 68 | Also edit and add vairus URLs to your streamer in Constants.py. Everything is described in comments. 69 | - After editing constants you can create a copy called AppConstants.py - This will be used before DefaultConstants.py. So you can pull changes without stashing or overwriting 70 | 5. Generate the database with 'python GenerateDatabase.py' or 'python3 GenerateDatabase.py' 71 | 6. Replace images in the image folder with those of your streamer, but keep same file names. 72 | - images/errIcon.png - icon used for embeds if there is an issue getting one from the platform, or if one doesn't exist 73 | - images/twitErrImg.jpg - default photo used for embeds if no other thumbnail or photo can be grabbed from the image list (if its empty) 74 | - images/avatars/calmStreamer.png - Bot avatar if streamer has streamed within the MIN_TIME_BEFORE_AVATAR_CHANGE time frame (default 48 hours) 75 | - images/avatars/pissedStreamer.png - Bot avatar if streamer hasn't streamed within the MIN_TIME_BEFORE_AVATAR_CHANGE time frame 76 | 7. Make sure you have python 3.10 installed. 77 | - make sure you have Chrome/Chromium another browser may work but not tested. 78 | - Make sure Xvfb is installed if on Linux. Xephyr, or Xvnc may work but untested 79 | - run command 'pip install -r requirements.txt' 80 | 8. Start the bot: 'python3 run.py' or 'python run.py' 81 | 82 | ### How To: Docker Compose or Portainer 83 | 84 | 1. Install Docker 85 | 2. Install Docker Compose and/or Portainer 86 | 3. Complete steps 1-6 above in the "how to set up the bot" section 87 | 4. Edit docker-compose.yml and follow the commented instructions 88 | 5. If using docker compose, from the Sassbot directory start it via "sudo docker compose up -d" 89 | 6. If using Portainer go to stacks > add stack 90 | - Copy paste over the non commented code from docker-compose.yml into the window 91 | - Deploy Stack 92 | 93 | ### Known Issues 94 | 95 | 1. If multiple accounts are streaming on the same platform at the same time, /rebroadcast will only show an announcement for one of the accounts. 96 | 2. Cam4 will ban your IP if you make too many calls from the same ip to the api. Make check times very long and/or rotate your ip 97 | 3. Fansly will 404 you for a period of time if you make too many requests from the same ip. 98 | 4. If the requesting ip is in a state that requires age verification, some of these checkers wont work. 99 | 100 | ### Update History 101 | - 3/27/2025 102 | - Fixed ManyVids checker 103 | - 12/17/2024 104 | - added two new commands 105 | - /ban-appeal - A command anyone can use to appeal a ban - works similarly to /confess but it's logged 106 | - /appeal-review - a command for white listed people to review the ban appeals. If approved, a message is sent to APPEAL_CHANNEL_ID to tell MOD_ROLE_ID to unban 107 | - new constants were added, so update appconstants.py as needed 108 | - 11/22/2024 109 | - Docker File improvements 110 | - fix for manyvids link 111 | - 10/22/2024 112 | - Added ManyVids Support 113 | - Because of this many things have changed in Default/App Constants so they will need to be updated with new vars 114 | - Added proxy support for most of the checkers. Anything that doesn't use nodriver will use socks5 115 | - Changed requirements update them with 'pip install -U -r requirements.txt' 116 | - Change nodriver temp file behavior. Uses default behavior in windows 117 | - Changed Dockerfile to improve size and fixed docker image to work with windows 118 | - 10/19/2024 119 | - Improvements on Twitch checker to hopefully work more quickly 120 | - Improvements with nodriver temp folders (was previously taking up too much disk space) 121 | - 10/12/2024 122 | - Moved more things to config to constants 123 | - Changing thumbnail behavior 124 | - Changing default avatar/Icon paths/images 125 | - Constants.py renamed to DefaultConstants.py 126 | - You can create your own version of Constants.py called AppConstants.py. Just copy over the contents of constants into this file, it's untracked and you can pull changes without stashing! 127 | - AppConstants.py is used before DefaultConstants.py 128 | - ghcr.io/bombg/sassbot:latest - now supports arm as well. The package ghcr.io/bombg/sassbot:latest-arm is no longer needed. 129 | - 10/11/2024 130 | - added workflow to auto make docker images. They are 131 | - ghcr.io/bombg/sassbot:latest 132 | - ghcr.io/bombg/sassbot:latest-arm 133 | - Moving prints over to pythyon logging library 134 | - Improved some exception handling 135 | - Fixed file descriptor leak-- happened over a week ago but i'm confident it's gone now. 136 | - added newer versions of hikari/tanjun to requirements. No code changes so old will still work too for now. 137 | - 10/3/2024 138 | - Fixed MFC, SC, BC checkers 139 | - 9/24/2024 140 | - Checkers that once used selenium now use nodriver instead. Chromedriver no longer required or supported 141 | - works with Chromium as well as Chrome. Other browsers are untested. 142 | - Added more reqirements nodriver,PyVirtualDisplay. Install them via the command 'pip install -U -r requirements.txt' 143 | - Xvfb is now required if using Linux. Xephyr or Xvnc may work but not tested. 144 | - Fixed a long standing bug with Chaturbate checker I just noticed; whoops. 145 | - One browser open at a time limit to work better on slower machines (have it working on a Raspberrypi 3B+) 146 | - Removed support for twitter (it's been broken for a while anyway) 147 | - Each platform has its own check timer in constants now. Instead of normal/long 148 | - Updated Dockerfile to reflect the new changes 149 | - Fixed Twitch checker 150 | - 8/02/2024 151 | - Added Docker support - Thanks jasmeralia! 152 | - Added /test-permission command that takes the channelId you wish to test permissions with as an input. 153 | - 5/03/2024 154 | - Added ePlay support (80% revenue share for the win) 155 | - 12/22/2023 156 | - Twitch checking hopefully fixed and turned back on 157 | - 12/16/2023 158 | - Temporarily disabling twitch detection until it's fixed. All channels are showing as live 159 | - 10/04/2023 160 | - Implemented the ability to post anonymous confessions/questions via /confess command. 161 | - These are unlogged server side 162 | - Must be approved by a whitelisted role/person via /confess-review 163 | - A new package is required to be installed so install it via the command 'pip install -U -r requirements.txt' 164 | - 9/29/2023 165 | - Expanded rerun support. 166 | - If a title contains a variation of rr/rerun/not live then the stream will be detected as a rerun. 167 | - reruns won't be announced by default, but you can turn on rerun-announcements with the /announce-rerun-toggle command 168 | - if ping-toggle is turned on you can also set roles to announce reruns to in constants.py 169 | - 9/18/2023 170 | - /title command added. It allows you to temporarily add a custom title for a platform/username combo. 171 | - title and platform arguments are required and with an optional accountName argument that is required if there's more than one account for that platform 172 | - TEMP_TITLE_UPTIME in constants.py determines how long a temp title added via /title will be used for. Current default is 16 hours 173 | - fix for Kick titles with ampersands 174 | - 9/14/2023 175 | - /users-graph command upgraded to include multiple days in a graph with an optional arguemnt. 176 | - 9/12/2023 177 | - Alerts can now happen for multiple accounts on the same platform at the same time. 178 | - 9/09/2023 179 | - Stream-status bug fix that was causing it to give strange values and not record data 180 | - Can mention other roles other than everyone and each platform can get their own custom mentions 181 | - everyone-ping-toggle renamed to ping-toggle 182 | - Can now have a custom notification channel per platform if you wish 183 | - 9/06/2023 184 | - Can now track multiple accounts per platform 185 | - 9/01/2023 186 | - Can now add individual user IDs to command white list as well as roles. 187 | - 8/xx/2023 188 | - Added StripChat Support 189 | - Added BongaCams Support 190 | - Added MyFreeCamsSupport 191 | - Added Cam4 Support 192 | -------------------------------------------------------------------------------- /checkers/Bongacams.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | from bs4 import BeautifulSoup 4 | import json 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | from utils.NoDriverBrowserCreator import getUserAgent 10 | import logging 11 | from utils.StaticMethods import GetThumbnail 12 | from utils.StaticMethods import GetProxies 13 | 14 | logger = logging.getLogger(__name__) 15 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 16 | 17 | def isModelOnline(bcUserName): 18 | title = Constants.bcDefaultTitle 19 | tempThumbUrl = "" 20 | isOnline = False 21 | icon = Constants.defaultIcon 22 | agent = getUserAgent() 23 | headers = {"User-Agent": agent} 24 | try: 25 | if Constants.BC_PROXY: 26 | page = requests.get(f'https://bongacams.com/{bcUserName}',headers=headers, proxies=GetProxies(Constants.BC_PROXY)) 27 | else: 28 | page = requests.get(f'https://bongacams.com/{bcUserName}',headers=headers) 29 | time.sleep(1) 30 | if page.status_code == 200: 31 | soup = BeautifulSoup(page.content, "html.parser") 32 | bcJson = [] 33 | roomJsons = soup.find_all("script", type="application/json") 34 | for roomJson in roomJsons: 35 | if 'chatTopicOptions' in roomJson.text: 36 | bcJson = json.loads(roomJson.text) 37 | break 38 | if bcJson: 39 | if bcJson["chatTopicOptions"]["currentTopic"]: 40 | title = bcJson["chatTopicOptions"]["currentTopic"] 41 | title = title.replace('\u200b', '') 42 | title = title.replace('\r', '') 43 | title = title.replace('\n', '') 44 | icon = bcJson['chatHeaderOptions']['profileImage'] 45 | icon = "https:" + icon 46 | isOnline = not bcJson['chatShowStatusOptions']['isOffline'] 47 | except requests.exceptions.ConnectTimeout: 48 | logger.warning("connection timed out to Bongacams. Bot detection or rate limited?") 49 | except requests.exceptions.SSLError: 50 | logger.warning("SSL Error when attempting to connect to Bomgacams") 51 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.bcThumbnail) 52 | return isOnline, title, thumbUrl, icon 53 | -------------------------------------------------------------------------------- /checkers/Cam4.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import json.decoder 4 | import time 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | from utils.NoDriverBrowserCreator import getUserAgent 10 | import logging 11 | from utils.StaticMethods import GetThumbnail 12 | from utils.StaticMethods import GetProxies 13 | 14 | logger = logging.getLogger(__name__) 15 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 16 | 17 | def isModelOnline(cam4UserName): 18 | title = Constants.cam4DefaultTitle 19 | tempThumbUrl = "" 20 | isOnline = False 21 | icon = Constants.defaultIcon 22 | agent = getUserAgent() 23 | try: 24 | headers = {"User-Agent": agent} 25 | if Constants.CAM4_PROXY: 26 | results = requests.get(f"https://www.cam4.com/rest/v1.0/search/performer/{cam4UserName}", headers=headers, proxies=GetProxies(Constants.CAM4_PROXY)) 27 | else: 28 | results = requests.get(f"https://www.cam4.com/rest/v1.0/search/performer/{cam4UserName}", headers=headers) 29 | time.sleep(1) 30 | try: 31 | cam4Json = results.json() 32 | if cam4Json['online']: 33 | isOnline = True 34 | icon = cam4Json['profileImageUrl'] 35 | except json.decoder.JSONDecodeError: 36 | logger.warning("cam4 api didn't respond?") 37 | except requests.exceptions.ConnectTimeout: 38 | logger.warning("connection timed out to cam4 api. Bot detection or rate limited?") 39 | except requests.exceptions.SSLError: 40 | logger.warning("SSL Error when attempting to connect to Cam4") 41 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.cam4Thumbnail) 42 | return isOnline, title, thumbUrl, icon -------------------------------------------------------------------------------- /checkers/Chaturbate.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | import json.decoder 8 | import logging 9 | from utils.StaticMethods import GetThumbnail 10 | 11 | logger = logging.getLogger(__name__) 12 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 13 | 14 | def isModelOnline(cbUserName): 15 | isOnline = False 16 | title = "placeholder cb title" 17 | tempThumbUrl = "" 18 | icon = Constants.defaultIcon 19 | try: 20 | onlineModels = requests.get(Constants.cbApiUrl) 21 | time.sleep(3) 22 | try: 23 | results = onlineModels.json()["results"] 24 | count = onlineModels.json()['count'] 25 | iterations = 1 26 | tempLimit = 0 27 | while count > tempLimit and not isOnline: 28 | tempLimit = Constants.cbJsonLimit * iterations 29 | for result in results: 30 | if result['username'] == cbUserName: 31 | isOnline = True 32 | title = result['room_subject'] 33 | tempThumbUrl = result['image_url'] + "?" + str(int(time.time())) 34 | break 35 | onlineModels = requests.get(Constants.cbApiUrl + f"&offset={tempLimit}") 36 | time.sleep(3) 37 | results = onlineModels.json()["results"] 38 | count = onlineModels.json()['count'] 39 | iterations = iterations + 1 40 | except json.decoder.JSONDecodeError: 41 | logger.warning("cb api didn't respond") 42 | except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError): 43 | logger.warning("connection timed out or aborted with Chaturbate. Bot detection or rate limited?") 44 | except requests.exceptions.SSLError: 45 | logger.warning("SSL Error when attempting to connect to Chaturbate") 46 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.cbThumbnail) 47 | return isOnline, title, thumbUrl, icon 48 | -------------------------------------------------------------------------------- /checkers/Eplay.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | from bs4 import BeautifulSoup 8 | import json 9 | import logging 10 | from utils.StaticMethods import GetThumbnail 11 | from utils.StaticMethods import GetProxies 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 15 | 16 | def isModelOnline(epUserName): 17 | isOnline = False 18 | title = Constants.epDefaultTitle 19 | tempThumbUrl = "" 20 | icon = Constants.defaultIcon 21 | try: 22 | if Constants.EP_PROXY: 23 | request = requests.get(f"https://eplay.com/{epUserName}", proxies=GetProxies(Constants.EP_PROXY)) 24 | else: 25 | request = requests.get(f"https://eplay.com/{epUserName}") 26 | time.sleep(1) 27 | soup = BeautifulSoup(request.content, "html.parser") 28 | profileJson = soup.find_all("script", type="application/json") 29 | profileJson = json.loads(profileJson[0].text) 30 | isOnline = profileJson["props"]["pageProps"]["dehydratedState"]["queries"][0]["state"]["data"]["live"] 31 | title = profileJson["props"]["pageProps"]["dehydratedState"]["queries"][0]["state"]["data"]["title"] 32 | title = title.replace('\u200b', '') 33 | title = title.replace('\r', '') 34 | title = title.replace('\n', '') 35 | tempThumbUrl = profileJson["props"]["pageProps"]["dehydratedState"]["queries"][0]["state"]["data"]["ss"] + "?" + str(int(time.time())) 36 | icon = profileJson["props"]["pageProps"]["dehydratedState"]["queries"][0]["state"]["data"]["avatar"] 37 | except requests.exceptions.ConnectTimeout: 38 | logger.warning("connection timed out to eplay.com. Bot detection or rate limited?") 39 | except requests.exceptions.SSLError: 40 | logger.warning("SSL Error when attempting to connect to Eplay") 41 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.epThumbnail) 42 | return isOnline, title, thumbUrl, icon -------------------------------------------------------------------------------- /checkers/Fansly.py: -------------------------------------------------------------------------------- 1 | try: 2 | from AppConstants import Constants as Constants 3 | except ImportError: 4 | from DefaultConstants import Constants as Constants 5 | import asyncio 6 | import nodriver as uc 7 | import utils.NoDriverBrowserCreator as ndb 8 | import globals 9 | import logging 10 | from utils.StaticMethods import GetThumbnail 11 | 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 14 | 15 | async def isModelOnline(fansUserName): 16 | fansUrl = f"https://fansly.com/{fansUserName}" 17 | tempThumbUrl = "" 18 | title = Constants.fansDefaultTitle 19 | isOnline = False 20 | icon = Constants.defaultIcon 21 | try: 22 | browser = await ndb.GetBrowser(proxy=Constants.FANS_PROXY) 23 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 24 | page = await browser.get(fansUrl) 25 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 26 | await ClickEnterButton(page) 27 | isOnline = await IsLiveBadge(page) 28 | icon = await GetIcon(page) 29 | await page.save_screenshot("Fansscreenshot.jpg") 30 | await page.close() 31 | await asyncio.sleep(.5*Constants.NODRIVER_WAIT_MULTIPLIER) 32 | browser.stop() 33 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 34 | globals.browserOpen = False 35 | except Exception as e: 36 | logger.warning(f"Error when getting browser for Fansly: {e}") 37 | globals.browserOpen = False 38 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.fansThumbnail) 39 | return isOnline, title, thumbUrl, icon 40 | 41 | async def ClickEnterButton(page:uc.Tab): 42 | try: 43 | enterBtn = await page.find("Enter",best_match=True) 44 | if enterBtn: 45 | await enterBtn.click() 46 | await asyncio.sleep(.5 * Constants.NODRIVER_WAIT_MULTIPLIER) 47 | except asyncio.exceptions.TimeoutError: 48 | pass 49 | 50 | async def IsLiveBadge(page:uc.Tab): 51 | live = False 52 | try: 53 | liveBadge = await page.find("live-badge bold font-size-sm", best_match=True) 54 | if liveBadge: 55 | live = True 56 | except asyncio.exceptions.TimeoutError: 57 | pass 58 | return live 59 | 60 | async def GetIcon(page:uc.Tab): 61 | icon = Constants.defaultIcon 62 | try: 63 | iconElements = await page.find_all("image cover") 64 | await iconElements[1].click() 65 | await asyncio.sleep(.5 * Constants.NODRIVER_WAIT_MULTIPLIER) 66 | iconElement = await page.find("image-overlay-flex", best_match=True) 67 | await iconElement.save_screenshot( "images/fansIcon.jpg") 68 | icon = "images/fansIcon.jpg" 69 | except asyncio.exceptions.TimeoutError: 70 | pass 71 | return icon 72 | -------------------------------------------------------------------------------- /checkers/Kick.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import nodriver as uc 3 | import json 4 | import time 5 | import utils.NoDriverBrowserCreator as ndb 6 | import globals 7 | try: 8 | from AppConstants import Constants as Constants 9 | except ImportError: 10 | from DefaultConstants import Constants as Constants 11 | import logging 12 | from utils.StaticMethods import GetThumbnail 13 | 14 | logger = logging.getLogger(__name__) 15 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 16 | 17 | async def isModelOnline(kickUserName): 18 | isOnline, title, tempThumbUrl, icon = setDefaultStreamValues() 19 | apiUrl = f"https://kick.com/api/v1/channels/{kickUserName}" 20 | try: 21 | browser = await ndb.GetBrowser(proxy=Constants.KICK_PROXY) 22 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 23 | page = await browser.get(apiUrl) 24 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 25 | await page.save_screenshot("KickScreenshot.jpg") 26 | content = await page.get_content() 27 | content = content.split('') 28 | if len(content) < 2: 29 | logger.warning("error with kick checker. user is banned,wrong username supplied, or cloudflare bot detection") 30 | else: 31 | jsonText = content[1].split('') 32 | isOnline, title, tempThumbUrl, icon = getStreamInfo(jsonText) 33 | await page.close() 34 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 35 | browser.stop() 36 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 37 | globals.browserOpen = False 38 | except Exception as e: 39 | logger.warning(f"error getting browser for Kick: {e}") 40 | globals.browserOpen = False 41 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.kickThumbnail) 42 | return isOnline, title, thumbUrl, icon 43 | 44 | def setDefaultStreamValues(): 45 | isOnline = False 46 | title = "place holder kick title, this should never show up unless coder fucked up" 47 | thumbUrl = "" 48 | icon = Constants.defaultIcon 49 | return isOnline, title, thumbUrl, icon 50 | 51 | def getStreamInfo(jsonText): 52 | isOnline, title, thumbUrl, icon = setDefaultStreamValues() 53 | try: 54 | results = json.loads(jsonText[0]) 55 | if results['livestream']: 56 | title = results['livestream']['session_title'] 57 | title = title.replace("&","&") 58 | title = title.replace("<", "<") 59 | thumbUrl = results['livestream']['thumbnail']['url']+ "#" + str(int(time.time())) 60 | icon = results['user']['profile_pic'] 61 | isOnline = True 62 | except json.decoder.JSONDecodeError: 63 | logger.warning("no json at kick api, bot detection site down, or cloudflare bot detection") 64 | return isOnline,title,thumbUrl,icon 65 | -------------------------------------------------------------------------------- /checkers/Manyvids.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | from utils.StaticMethods import GetThumbnail 8 | from utils.StaticMethods import GetProxies 9 | import logging 10 | import re 11 | from utils.NoDriverBrowserCreator import getUserAgent 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 15 | 16 | def isModelOnline(mvUserName): 17 | title = Constants.mvDefaultTitle 18 | tempThumbUrl = '' 19 | isOnline = False 20 | icon = Constants.defaultIcon 21 | pageUrl = f"https://www.manyvids.com/live/cam/{mvUserName.lower()}" 22 | agent = getUserAgent() 23 | headers = {"User-Agent": agent} 24 | if Constants.MV_PROXY: 25 | page = requests.get(pageUrl, proxies=GetProxies(Constants.MV_PROXY), headers=headers) 26 | else: 27 | page = requests.get(pageUrl, headers=headers) 28 | soup = BeautifulSoup(page.content, "html.parser") 29 | onlineStatus = soup.find("div", {"class":"status_box__v1drl"}) 30 | if onlineStatus: 31 | logger.debug(onlineStatus.text) 32 | else: 33 | logger.debug("no online status") 34 | if onlineStatus and (onlineStatus.text == "LIVE" or onlineStatus.text == "IN PRIVATE"): 35 | isOnline = True 36 | icon = GetIcon(soup, mvUserName) 37 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.mvThumbnail) 38 | return isOnline, title, thumbUrl, icon 39 | 40 | def GetIcon(soup:BeautifulSoup, mvUserName): 41 | icon = Constants.defaultIcon 42 | reString = r"https:\/\/cdn5\.manyvids\.com\/php_uploads\/profile\/" + mvUserName + r"\/image\/cropped-image_\d+.jpeg" 43 | icon = re.search(reString, soup.prettify()) 44 | if icon: 45 | icon = icon.group() 46 | return icon 47 | -------------------------------------------------------------------------------- /checkers/Myfreecams.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | from bs4 import BeautifulSoup 8 | import logging 9 | from utils.StaticMethods import GetThumbnail 10 | from utils.StaticMethods import GetProxies 11 | 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 14 | 15 | def isModelOnline(mfcUserName): 16 | isOnline = False 17 | title = Constants.mfcDefaultTitle 18 | tempThumbUrl = "" 19 | icon = Constants.defaultIcon 20 | try: 21 | if Constants.MFC_PROXY: 22 | request = requests.get(f"https://share.myfreecams.com/{mfcUserName}", proxies=GetProxies(Constants.MFC_PROXY)) 23 | else: 24 | request = requests.get(f"https://share.myfreecams.com/{mfcUserName}") 25 | time.sleep(1) 26 | soup = BeautifulSoup(request.content, "html.parser") 27 | vidPreview = soup.find(class_='campreview d-none') 28 | if vidPreview: 29 | isOnline = True 30 | icon = soup.find(class_='avatar online').find("img")['src'] if soup.find(class_='avatar online') else soup.find(class_='avatar').find("img")['src'] 31 | except requests.exceptions.ConnectTimeout: 32 | logger.warning("connection timed out to share.myfreecams.com. Bot detection or rate limited?") 33 | except requests.exceptions.SSLError: 34 | logger.warning("SSL Error when attempting to connect to MyFreeCams") 35 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.mfcThumbnail) 36 | return isOnline, title, thumbUrl, icon -------------------------------------------------------------------------------- /checkers/Onlyfans.py: -------------------------------------------------------------------------------- 1 | try: 2 | from AppConstants import Constants as Constants 3 | except ImportError: 4 | from DefaultConstants import Constants as Constants 5 | import re 6 | import asyncio 7 | import nodriver as uc 8 | import utils.NoDriverBrowserCreator as ndb 9 | import globals 10 | import logging 11 | from utils.StaticMethods import GetThumbnail 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 15 | 16 | async def isModelOnline(ofUserName): 17 | isOnline = False 18 | ofUrl = f"https://onlyfans.com/{ofUserName}" 19 | title = Constants.ofDefaultTitle 20 | tempThumbUrl = "" 21 | icon = Constants.defaultIcon 22 | try: 23 | browser = await ndb.GetBrowser(proxy=Constants.OF_PROXY) 24 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 25 | page = await browser.get(ofUrl, new_window=True) 26 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 27 | await page.save_screenshot("Ofscreenshot.jpg") 28 | isOnline = await IsLiveBadge(page) 29 | icon = await GetIcon(page) 30 | await page.close() 31 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 32 | browser.stop() 33 | await asyncio.sleep(1*Constants.NODRIVER_WAIT_MULTIPLIER) 34 | globals.browserOpen = False 35 | except Exception as e: 36 | logger.warning(f"Error getting browser for Onylyfans: {e}") 37 | globals.browserOpen = False 38 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.ofThumbnail) 39 | return isOnline, title, thumbUrl, icon 40 | 41 | async def GetIcon(page:uc.Tab): 42 | icon = Constants.defaultIcon 43 | reString = r'^https:\/\/.+avatar.jpg$' 44 | try: 45 | imageElements = await page.find_all("data-v-325c6981") 46 | for element in imageElements: 47 | if element.attrs.get("src") and re.search(reString, element.attrs.get("src")): 48 | icon = element.attrs.get("src") 49 | except asyncio.exceptions.TimeoutError: 50 | pass 51 | return icon 52 | 53 | async def IsLiveBadge(page:uc.Tab): 54 | live = False 55 | try: 56 | liveBadge = await page.find("g-avatar__icon m-live", best_match=True) 57 | if liveBadge: 58 | live = True 59 | except asyncio.exceptions.TimeoutError: 60 | pass 61 | return live -------------------------------------------------------------------------------- /checkers/Stripchat.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import time 4 | try: 5 | from AppConstants import Constants as Constants 6 | except ImportError: 7 | from DefaultConstants import Constants as Constants 8 | from utils.NoDriverBrowserCreator import getUserAgent 9 | import logging 10 | from utils.StaticMethods import GetThumbnail 11 | from utils.StaticMethods import GetProxies 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 15 | 16 | def isModelOnline(scUserName): 17 | title = Constants.scDefaultTitle 18 | tempThumbUrl = '' 19 | isOnline = False 20 | icon = Constants.defaultIcon 21 | agent = getUserAgent() 22 | headers = {"User-Agent": agent} 23 | try: 24 | if Constants.SC_PROXY: 25 | page = requests.get(f'https://stripchat.com/api/vr/v2/models/username/{scUserName}', headers=headers, proxies=GetProxies(Constants.SC_PROXY)) 26 | else: 27 | page = requests.get(f'https://stripchat.com/api/vr/v2/models/username/{scUserName}', headers=headers) 28 | time.sleep(1) 29 | if page.status_code == 200: 30 | try: 31 | scJson = page.json() 32 | isOnline = True if scJson['model']['status'] != 'off' else False 33 | icon = scJson['model']['avatarUrl'] 34 | title = scJson['goal']['description'] if scJson['goal']['description'] else Constants.scDefaultTitle 35 | tempThumbUrl = scJson['model']['previewUrl'] + "?" + str(int(time.time())) 36 | except json.decoder.JSONDecodeError: 37 | pass 38 | except requests.exceptions.ConnectTimeout: 39 | logger.warning("connection timed out to Stripchat. Bot detection or rate limited?") 40 | except requests.exceptions.SSLError: 41 | logger.warning("SSL Error when attempting to connect to Stripchat") 42 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.scThumbnail) 43 | return isOnline, title, thumbUrl, icon -------------------------------------------------------------------------------- /checkers/Twitch.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from bs4 import BeautifulSoup 4 | import time 5 | import logging 6 | try: 7 | from AppConstants import Constants as Constants 8 | except ImportError: 9 | from DefaultConstants import Constants as Constants 10 | from utils.StaticMethods import GetThumbnail 11 | 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 14 | 15 | def isModelOnline(twitchChannelName: str): 16 | twitchChannelName = twitchChannelName.lower() 17 | title = "placeholder twitch title" 18 | tempThumbUrl = '' 19 | isOnline = False 20 | icon = Constants.defaultIcon 21 | try: 22 | isOnline = IsOnline(twitchChannelName) 23 | time.sleep(1) 24 | if isOnline: 25 | isOnline = True 26 | page = requests.get(f'https://www.twitch.tv/{twitchChannelName}') 27 | tempThumbUrl = f'https://static-cdn.jtvnw.net/previews-ttv/live_user_{twitchChannelName}-640x360.jpg' 28 | time.sleep(1) 29 | soup = BeautifulSoup(page.content, "html.parser") 30 | title = getTitle(soup) 31 | reticon = getIcon(soup) 32 | if reticon: 33 | icon = reticon 34 | tempThumbUrl = tempThumbUrl + "?" + str(int(time.time())) 35 | except requests.exceptions.ConnectTimeout: 36 | logger.warning("connection timed out to Twitch. Bot detection or rate limited?") 37 | except requests.exceptions.SSLError: 38 | logger.warning("SSL Error when attempting to connect to Twitch") 39 | except TypeError: 40 | logger.warning("twitch user banned or doesn't exist") 41 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.twitchThumbnail) 42 | return isOnline, title, thumbUrl, icon 43 | 44 | def getIcon(soup): 45 | icon = 0 46 | try: 47 | icon = soup.find("meta", property="og:image")['content'] 48 | except IndexError: 49 | pass 50 | return icon 51 | 52 | def getTitle(soup): 53 | title = "placeholder twitch title" 54 | try: 55 | title = soup.find("meta", property="og:description")['content'] 56 | except IndexError: 57 | pass 58 | return title 59 | 60 | def IsOnline(channelName): 61 | url = "https://gql.twitch.tv/gql" 62 | query = "query {\n user(login: \""+ channelName +"\") {\n stream {\n id\n }\n }\n}" 63 | return True if requests.request("POST", url, json={"query": query, "variables": {}}, headers={"client-id": "kimne78kx3ncx6brgo4mv6wki5h1ko"}).json()["data"]["user"]["stream"] else False 64 | -------------------------------------------------------------------------------- /checkers/Youtube.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import json 4 | import logging 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | import time 10 | from utils.StaticMethods import GetThumbnail 11 | 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 14 | 15 | def isModelOnline(ytUserName): 16 | ytUrl = f"https://www.youtube.com/@{ytUserName}/live" 17 | online = False 18 | title = "placeholder youtube title" 19 | tempThumbUrl = "" 20 | icon = Constants.defaultIcon 21 | try: 22 | page = requests.get(ytUrl, cookies={'CONSENT': 'YES+42'}) 23 | soup = BeautifulSoup(page.content, "html.parser") 24 | live = soup.find("link", {"rel": "canonical"}) 25 | scripts = soup.find_all('script') 26 | liveJson = getLiveJson(scripts) 27 | if liveJson: 28 | iconJson = getIconJson(scripts) 29 | status = liveJson["playabilityStatus"]["status"] 30 | title = liveJson["videoDetails"]['title'] 31 | tempThumbUrl = liveJson['videoDetails']['thumbnail']['thumbnails'][4]['url'] + "?" + str(int(time.time())) 32 | if live and status != "LIVE_STREAM_OFFLINE": 33 | online = True 34 | if iconJson: 35 | icon = iconJson['contents']['twoColumnWatchNextResults']['results']['results']['contents'][1]['videoSecondaryInfoRenderer']['owner']['videoOwnerRenderer']['thumbnail']['thumbnails'][0]['url'] 36 | except requests.exceptions.ConnectTimeout: 37 | logger.warning("connection timed out to Youtube. Bot detection or rate limited?") 38 | except requests.exceptions.SSLError: 39 | logger.warning("SSL Error when attempting to connect to Youtube") 40 | thumbUrl = GetThumbnail(tempThumbUrl, Constants.ytThumbnail) 41 | return online,title, thumbUrl, icon 42 | 43 | def getIconJson(scripts): 44 | iconJson = 0 45 | try: 46 | ytJson2 = str(scripts).split('var ytInitialData = ') 47 | splitJson2 = str(ytJson2[1]).split(";") 48 | iconJson = json.loads(splitJson2[0]) 49 | except IndexError: 50 | pass 51 | return iconJson 52 | 53 | def getLiveJson(scripts): 54 | liveJson = 0 55 | try: 56 | ytJson = str(scripts).split('var ytInitialPlayerResponse = ') 57 | splitJson = str(ytJson[1]).split(";") 58 | liveJson = json.loads(splitJson[0]) 59 | except IndexError: 60 | pass 61 | return liveJson -------------------------------------------------------------------------------- /decorators/CommandLogger.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | import tanjun 3 | from datetime import datetime 4 | import time 5 | import logging 6 | try: 7 | from AppConstants import Constants as Constants 8 | except ImportError: 9 | from DefaultConstants import Constants as Constants 10 | 11 | logger = logging.getLogger(__name__) 12 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 13 | 14 | class CommandLogger: 15 | def __init__(self, func) -> None: 16 | self.func = func 17 | def __call__(self, *args: Any, **kwds: Any) -> Any: 18 | self.file = open("commandLogs.txt", 'a') 19 | self.date = datetime.fromtimestamp(time.time()) 20 | ctx = args[0] 21 | if isinstance(ctx, tanjun.abc.SlashContext): 22 | self.file.write(f"{self.date} - {self.func.__name__} - used by {ctx.member.id} aka {ctx.member.display_name}\n") 23 | else: 24 | logger.error("didn't get right ctx for command logger") 25 | self.file.close() 26 | return self.func(*args,**kwds) -------------------------------------------------------------------------------- /decorators/Permissions.py: -------------------------------------------------------------------------------- 1 | import tanjun 2 | import logging 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | 8 | logger = logging.getLogger(__name__) 9 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 10 | 11 | 12 | class Permissions(object): 13 | def __init__(self, rolesList): 14 | self.rolesList = rolesList 15 | def __call__(self, function): 16 | async def wrapper(*args, **kwds): 17 | ctx = args[0] 18 | if isinstance(ctx,tanjun.abc.SlashContext): 19 | hasPermission = False 20 | roles = ctx.member.get_roles() 21 | for role in roles: 22 | if role.id in self.rolesList or ctx.member.id in self.rolesList: 23 | hasPermission = True 24 | if hasPermission: 25 | value = function(*args, **kwds) 26 | return await value 27 | else: 28 | await ctx.respond("You don't have permission to do this") 29 | else: 30 | logger.warning("Improper use of permissions decorator") 31 | return wrapper -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This exmple works on my machine but obviously /home/bombg/Repos won't be on yours. 2 | # So change /home/bombg/Repos/SassBot to wherever sassbot is located on your machine 3 | # I.E if your local machine Sassbot is in /Example/Repos/SassBot then 4 | # /home/bombg/Repos/SassBot/Constants.py:/opt/SassBot/Constants.py 5 | # Becomes 6 | # /Example/Repos/SassBot/Constants.py:/opt/SassBot/Constants.py -- Right side of the colon stays the same 7 | # Make sure you've edited Constants, created the /secrets/token file AND generated the database before you start the image (Follow intructions Steps 1-6) 8 | services: 9 | sassbot: 10 | image: ghcr.io/bombg/sassbot:latest 11 | volumes: 12 | - /home/bombg/Repos/SassBot/AppConstants.py:/opt/SassBot/AppConstants.py 13 | - /home/bombg/Repos/SassBot/secrets:/opt/SassBot/secrets 14 | - /home/bombg/Repos/SassBot/sassBot.db:/opt/SassBot/sassBot.db 15 | - /home/bombg/Repos/SassBot/Fae/FaeIcon.png:/opt/SassBot/images/errIcon.png 16 | - /home/bombg/Repos/SassBot/Fae/FaeErr.jpg:/opt/SassBot/images/twitErrImg.jpg 17 | - /home/bombg/Repos/SassBot/Fae/FaeC.png:/opt/SassBot/images/avatars/calmStreamer.png 18 | - /home/bombg/Repos/SassBot/Fae/FaeP.png:/opt/SassBot/images/avatars/pissedStreamer.png 19 | restart: unless-stopped 20 | 21 | # The example below is if you want to look at images nodriver has created for troubleshooting purposes 22 | # Make sure you touch create the jpg files before you start the image or they'll be created as folders (and be useless) 23 | # 24 | # services: 25 | # sassbot: 26 | # image: ghcr.io/bombg/sassbot:latest 27 | # volumes: 28 | # - /home/bombg/Repos/SassBot/AppConstants.py:/opt/SassBot/AppConstants.py 29 | # - /home/bombg/Repos/SassBot/secrets:/opt/SassBot/secrets 30 | # - /home/bombg/Repos/SassBot/sassBot.db:/opt/SassBot/sassBot.db 31 | # - /home/bombg/Repos/SassBot/Fae/FaeIcon.png:/opt/SassBot/images/errIcon.png 32 | # - /home/bombg/Repos/SassBot/Fae/FaeErr.jpg:/opt/SassBot/images/twitErrImg.jpg 33 | # - /home/bombg/Repos/SassBot/Fae/FaeC.png:/opt/SassBot/images/avatars/calmStreamer.png 34 | # - /home/bombg/Repos/SassBot/Fae/FaeP.png:/opt/SassBot/images/avatars/pissedStreamer.png 35 | # - /home/bombg/Repos/SassBot/Fansscreenshot.jpg:/opt/SassBot/Fansscreenshot.jpg 36 | # - /home/bombg/Repos/SassBot/Ofscreenshot.jpg:/opt/SassBot/Ofscreenshot.jpg 37 | # - /home/bombg/Repos/SassBot/KickScreenshot.jpg:/opt/SassBot/KickScreenshot.jpg 38 | # restart: unless-stopped 39 | 40 | 41 | # Yet another version that includes a restarter container to reboot the container once a day - be sure to rename `faebot-sassbot-1` to the name of your actual bot container 42 | 43 | # services: 44 | # sassbot: 45 | # image: ghcr.io/bombg/sassbot:latest 46 | # volumes: 47 | # - /home/bombg/Repos/SassBot/AppConstants.py:/opt/SassBot/AppConstants.py 48 | # - /home/bombg/Repos/SassBot/secrets:/opt/SassBot/secrets 49 | # - /home/bombg/Repos/SassBot/sassBot.db:/opt/SassBot/sassBot.db 50 | # - /home/bombg/Repos/SassBot/Fae/FaeIcon.png:/opt/SassBot/images/errIcon.png 51 | # - /home/bombg/Repos/SassBot/Fae/FaeErr.jpg:/opt/SassBot/images/twitErrImg.jpg 52 | # - /home/bombg/Repos/SassBot/Fae/FaeC.png:/opt/SassBot/images/avatars/calmStreamer.png 53 | # - /home/bombg/Repos/SassBot/Fae/FaeP.png:/opt/SassBot/images/avatars/pissedStreamer.png 54 | # - /home/bombg/Repos/SassBot/Fansscreenshot.jpg:/opt/SassBot/Fansscreenshot.jpg 55 | # - /home/bombg/Repos/SassBot/Ofscreenshot.jpg:/opt/SassBot/Ofscreenshot.jpg 56 | # - /home/bombg/Repos/SassBot/KickScreenshot.jpg:/opt/SassBot/KickScreenshot.jpg 57 | # restart: unless-stopped 58 | # restarter: 59 | # image: docker:cli 60 | # volumes: ["/var/run/docker.sock:/var/run/docker.sock"] 61 | # command: ["/bin/sh", "-c", "while true; do sleep 86400; docker restart faebot-sassbot-1; done"] 62 | # restart: unless-stopped 63 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python -u ./run.py 2>&1 -------------------------------------------------------------------------------- /globals.py: -------------------------------------------------------------------------------- 1 | try: 2 | from AppConstants import Constants as Constants 3 | except ImportError: 4 | from DefaultConstants import Constants as Constants 5 | import time 6 | 7 | globalPlayString = "" 8 | normalAvtar = False 9 | 10 | botStartTime = time.time() 11 | 12 | rebroadcast = { 13 | "chaturbate":0, 14 | "onlyfans":0, 15 | "fansly":0, 16 | "twitch":0, 17 | "youtube":0, 18 | "kick":0, 19 | "cam4":0, 20 | "mfc":0, 21 | "bongacams":0, 22 | "stripchat":0, 23 | "eplay":0, 24 | "manyvids":0 25 | } 26 | 27 | confessionIds = {"alert":0} 28 | appealIds = {"alert":0} 29 | 30 | browserOpen = False -------------------------------------------------------------------------------- /images/avatars/calmStreamer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/avatars/calmStreamer.png -------------------------------------------------------------------------------- /images/avatars/pissedStreamer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/avatars/pissedStreamer.png -------------------------------------------------------------------------------- /images/errIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/errIcon.png -------------------------------------------------------------------------------- /images/platformImages/CbImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/CbImage.png -------------------------------------------------------------------------------- /images/platformImages/FansImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/FansImage.png -------------------------------------------------------------------------------- /images/platformImages/KickImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/KickImage.png -------------------------------------------------------------------------------- /images/platformImages/OFImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/OFImage.png -------------------------------------------------------------------------------- /images/platformImages/bcImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/bcImage.png -------------------------------------------------------------------------------- /images/platformImages/cam4Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/cam4Image.png -------------------------------------------------------------------------------- /images/platformImages/epImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/epImage.png -------------------------------------------------------------------------------- /images/platformImages/mfcImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/mfcImage.png -------------------------------------------------------------------------------- /images/platformImages/mvImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/mvImage.png -------------------------------------------------------------------------------- /images/platformImages/scImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/scImage.png -------------------------------------------------------------------------------- /images/platformImages/twitchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/twitchImage.png -------------------------------------------------------------------------------- /images/platformImages/ytImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/platformImages/ytImage.png -------------------------------------------------------------------------------- /images/twitErrImg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/images/twitErrImg.jpg -------------------------------------------------------------------------------- /plugins/checks.py: -------------------------------------------------------------------------------- 1 | import tanjun 2 | import alluka 3 | import hikari 4 | import asyncio 5 | import checkers.Chaturbate as Chaturbate 6 | import checkers.Onlyfans as Onlyfans 7 | import checkers.Fansly as Fansly 8 | import checkers.Myfreecams as MFC 9 | import checkers.Bongacams as BC 10 | import checkers.Stripchat as SC 11 | import checkers.Eplay as EP 12 | import checkers.Manyvids as MV 13 | try: 14 | from AppConstants import Constants as Constants 15 | except ImportError: 16 | from DefaultConstants import Constants as Constants 17 | import checkers.Twitch as Twitch 18 | import checkers.Kick as Kick 19 | import checkers.Youtube as Youtube 20 | import checkers.Cam4 as Cam4 21 | import globals 22 | import time 23 | import utils.StaticMethods as StaticMethods 24 | from utils.Notifications import Notifications 25 | from utils.Database import Database 26 | from typing import Callable 27 | from datetime import date 28 | import inspect 29 | import logging 30 | 31 | component = tanjun.Component() 32 | logger = logging.getLogger(__name__) 33 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 34 | 35 | @tanjun.as_loader 36 | def load(client: tanjun.abc.Client) -> None: 37 | client.add_component(component.copy()) 38 | 39 | async def platformChecker(isOnlineFunc: Callable,platformNotifFunc: Callable, userName: str, platformName: str, rest: hikari.impl.RESTClientImpl): 40 | try: 41 | if inspect.iscoroutinefunction(isOnlineFunc): 42 | isOnline, title, thumbUrl, icon = await isOnlineFunc(userName) 43 | else: 44 | isOnline, title, thumbUrl, icon = await asyncio.get_running_loop().run_in_executor(None,isOnlineFunc,userName) 45 | except Exception as e: 46 | logging.exception(f"caught exception: {e}") 47 | thumbUrl = "" 48 | title = "NoTitle" 49 | isOnline = False 50 | icon = Constants.defaultIcon 51 | isRerun = False 52 | db = Database() 53 | lastOnlineMessage,streamStartTime,streamEndTime = db.getPlatformAccountsRowValues(platformName,userName) 54 | tempTitle, tempTitleTime = db.getPlatformTempTitle(platformName, userName) 55 | secondsSinceTempTitle = StaticMethods.timeToSeconds(tempTitleTime) 56 | secondsSinceLastMessage = StaticMethods.timeToSeconds(lastOnlineMessage) 57 | secondsSinceStreamEndTime = StaticMethods.timeToSeconds(streamEndTime) 58 | secondsSinceStreamStartTime = StaticMethods.timeToSeconds(streamStartTime) 59 | if tempTitle and secondsSinceTempTitle < Constants.TEMP_TITLE_UPTIME: 60 | title = tempTitle 61 | if isOnline and StaticMethods.isRerun(title): 62 | isOnline = isOnline if db.getRerunAnnounce() else False 63 | isRerun = True 64 | logger.debug(platformName + " +Offline|-Online: " + str((-1 * secondsSinceStreamStartTime) if isOnline else secondsSinceStreamEndTime)) 65 | if isOnline == True: 66 | db.setRerun(isRerun, platformName) 67 | if secondsSinceStreamEndTime >= Constants.WAIT_BETWEEN_MESSAGES and secondsSinceLastMessage >= Constants.WAIT_BETWEEN_MESSAGES and streamEndTime >= streamStartTime: 68 | logger.info(f"{platformName}: Sending Notification") 69 | await platformNotifFunc(rest, title, thumbUrl, icon, userName, isRerun) 70 | db.updatePlatformRowCol(platformName,"last_stream_start_time",time.time()) 71 | db.updatePlatformAccountRowCol(platformName, userName,"last_stream_start_time",time.time()) 72 | globals.rebroadcast[platformName] = 0 73 | elif secondsSinceLastMessage >= Constants.ONLINE_MESSAGE_REBROADCAST_TIME or globals.rebroadcast[platformName]: 74 | logger.info(f"{platformName}: Rebroadcast Command or Rebroadcast_TIME Notification sent") 75 | await platformNotifFunc(rest, title, thumbUrl, icon, userName, isRerun) 76 | lastOnlineMessage = time.time() 77 | globals.rebroadcast[platformName] = 0 78 | elif streamEndTime >= streamStartTime: 79 | db.updatePlatformRowCol(platformName,"last_stream_start_time",time.time()) 80 | db.updatePlatformAccountRowCol(platformName,userName,"last_stream_start_time",time.time()) 81 | elif isOnline == False: 82 | db.setRerun(isRerun, platformName) 83 | if streamEndTime <= streamStartTime: 84 | db.updatePlatformRowCol(platformName,"last_stream_end_time",time.time()) 85 | db.updatePlatformAccountRowCol(platformName,userName,"last_stream_end_time",time.time()) 86 | globals.rebroadcast[platformName] = 0 87 | 88 | @component.with_schedule 89 | @tanjun.as_interval(Constants.CB_CHECK_TIMER) 90 | async def checkChatur(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 91 | if Constants.cbUserName: 92 | for cbUserName in Constants.cbUserName: 93 | await platformChecker(Chaturbate.isModelOnline, Notifications.ChaturNotification,cbUserName,"chaturbate",rest) 94 | await asyncio.sleep(Constants.CB_CHECK_TIMER/len(Constants.cbUserName)) 95 | 96 | @component.with_schedule 97 | @tanjun.as_interval(Constants.OF_CHECK_TIMER) 98 | async def checkOnlyfans(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 99 | if Constants.ofUserName: 100 | for ofUserName in Constants.ofUserName: 101 | await platformChecker(Onlyfans.isModelOnline, Notifications.OFNotification,ofUserName,"onlyfans",rest) 102 | await asyncio.sleep(Constants.OF_CHECK_TIMER/len(Constants.ofUserName)) 103 | 104 | @component.with_schedule 105 | @tanjun.as_interval(Constants.FANS_CHECK_TIMER) 106 | async def checkFansly(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 107 | if Constants.fansUserName: 108 | for fansUserName in Constants.fansUserName: 109 | await platformChecker(Fansly.isModelOnline, Notifications.FansNotification,fansUserName,"fansly",rest) 110 | await asyncio.sleep(Constants.FANS_CHECK_TIMER/len(Constants.fansUserName)) 111 | 112 | @component.with_schedule 113 | @tanjun.as_interval(Constants.TWITCH_CHECK_TIMER) 114 | async def checkTwitch(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 115 | if Constants.twitchUserName: 116 | for twitchUserName in Constants.twitchUserName: 117 | await platformChecker(Twitch.isModelOnline, Notifications.TwitchNotification,twitchUserName,"twitch",rest) 118 | await asyncio.sleep(Constants.TWITCH_CHECK_TIMER/len(Constants.twitchUserName)) 119 | 120 | @component.with_schedule 121 | @tanjun.as_interval(Constants.YT_CHECK_TIMER) 122 | async def checkYT(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 123 | if Constants.ytUserName: 124 | for ytUserName in Constants.ytUserName: 125 | await platformChecker(Youtube.isModelOnline, Notifications.YTNotification,ytUserName,"youtube",rest) 126 | await asyncio.sleep(Constants.YT_CHECK_TIMER/len(Constants.ytUserName)) 127 | 128 | @component.with_schedule 129 | @tanjun.as_interval(Constants.KICK_CHECK_TIMER) 130 | async def checkKick(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 131 | if Constants.kickUserName: 132 | for kickUserName in Constants.kickUserName: 133 | await platformChecker(Kick.isModelOnline, Notifications.KickNotification,kickUserName,"kick",rest) 134 | await asyncio.sleep(Constants.KICK_CHECK_TIMER/len(Constants.kickUserName)) 135 | 136 | @component.with_schedule 137 | @tanjun.as_interval(Constants.CAM4_CHECK_TIMER) 138 | async def checkCam4(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 139 | if Constants.cam4UserName: 140 | for cam4UserName in Constants.cam4UserName: 141 | await platformChecker(Cam4.isModelOnline, Notifications.Cam4Notification,cam4UserName,"cam4",rest) 142 | await asyncio.sleep(Constants.CAM4_CHECK_TIMER/len(Constants.cam4UserName)) 143 | 144 | @component.with_schedule 145 | @tanjun.as_interval(Constants.MFC_CHECK_TIMER) 146 | async def checkMfc(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 147 | if Constants.mfcUserName: 148 | for mfcUserName in Constants.mfcUserName: 149 | await platformChecker(MFC.isModelOnline, Notifications.MfcNotification,mfcUserName,"mfc",rest) 150 | await asyncio.sleep(Constants.MFC_CHECK_TIMER/len(Constants.mfcUserName)) 151 | 152 | 153 | @component.with_schedule 154 | @tanjun.as_interval(Constants.BC_CHECK_TIMER) 155 | async def checkBc(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 156 | if Constants.bcUserName: 157 | for bcUserName in Constants.bcUserName: 158 | await platformChecker(BC.isModelOnline, Notifications.BcNotification,bcUserName,"bongacams",rest) 159 | await asyncio.sleep(Constants.BC_CHECK_TIMER/len(Constants.bcUserName)) 160 | 161 | 162 | @component.with_schedule 163 | @tanjun.as_interval(Constants.SC_CHECK_TIMER) 164 | async def checkSc(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 165 | if Constants.scUserName: 166 | for scUserName in Constants.scUserName: 167 | await platformChecker(SC.isModelOnline, Notifications.ScNotification,scUserName,"stripchat",rest) 168 | await asyncio.sleep(Constants.SC_CHECK_TIMER/len(Constants.scUserName)) 169 | 170 | @component.with_schedule 171 | @tanjun.as_interval(Constants.EP_CHECK_TIMER) 172 | async def checkEp(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 173 | if Constants.epUserName: 174 | for epUserName in Constants.epUserName: 175 | await platformChecker(EP.isModelOnline, Notifications.EpNotification,epUserName,"eplay",rest) 176 | await asyncio.sleep(Constants.EP_CHECK_TIMER/len(Constants.epUserName)) 177 | 178 | @component.with_schedule 179 | @tanjun.as_interval(Constants.MV_CHECK_TIMER) 180 | async def checkMv(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 181 | if Constants.mvUserName: 182 | for mvUserName in Constants.mvUserName: 183 | await platformChecker(MV.isModelOnline, Notifications.MvNotification,mvUserName,"manyvids",rest) 184 | await asyncio.sleep(Constants.MV_CHECK_TIMER/len(Constants.mvUserName)) 185 | 186 | 187 | @component.with_schedule 188 | @tanjun.as_interval(Constants.AVATAR_CHECK_TIMER) 189 | async def changeAvatar(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 190 | db = Database() 191 | online = StaticMethods.checkOnline(db) 192 | onTime, offTime, totalOnTime = db.getStreamTableValues() 193 | hours, minutes = StaticMethods.timeToHoursMinutes(offTime) 194 | if online and not globals.normalAvtar: 195 | await rest.edit_my_user(avatar = Constants.calmAvatar) 196 | logger.info(f"changed avatar to good {Constants.streamerName}") 197 | globals.normalAvtar = True 198 | if not online and globals.normalAvtar and hours >= Constants.MIN_TIME_BEFORE_AVATAR_CHANGE and offTime != 0: 199 | await rest.edit_my_user(avatar = Constants.pissedAvatar) 200 | logger.info(f"changed avatar to bad {Constants.streamerName}") 201 | globals.normalAvtar = False 202 | 203 | @component.with_schedule 204 | @tanjun.as_interval(Constants.STATUS_CHECK_TIMER) 205 | async def changeStatus(bot: alluka.Injected[hikari.GatewayBot]) -> None: 206 | db = Database() 207 | subathon,subStart,subEnd = db.getSubathonStatusClean() 208 | playingString = StaticMethods.checkOnline(db) 209 | if subathon: 210 | hours, minutes = StaticMethods.timeToHoursMinutes(subStart) 211 | playingString = playingString + "athon H:" + str(hours) + "M:" +str(minutes) + " " 212 | if not playingString: 213 | playingString = playingString + "Offline " 214 | if playingString != globals.globalPlayString: 215 | logger.info("Updated presence to " + playingString) 216 | globals.globalPlayString = playingString 217 | await asyncio.sleep(5) 218 | await bot.update_presence(activity=hikari.Activity( 219 | name = playingString, 220 | type = hikari.ActivityType.STREAMING, 221 | url = Constants.twitchUrl 222 | )) 223 | await asyncio.sleep(5) 224 | 225 | @component.with_schedule 226 | @tanjun.as_interval(Constants.STATUS_CHECK_TIMER) 227 | async def checkOnlineTime() -> None: 228 | db = Database() 229 | online = StaticMethods.checkOnline(db) 230 | lastOnline,lastOffline,totalStreamTime = db.getStreamTableValues() 231 | if online and lastOffline >= lastOnline: 232 | logger.info("time online starts now") 233 | db.setStreamLastOnline(time.time()) 234 | elif not online and lastOffline <= lastOnline: 235 | logger.info("offline time starts now") 236 | StaticMethods.setOfflineAddTime() 237 | 238 | @component.with_schedule 239 | @tanjun.as_time_schedule(minutes=[5,15,25,35,45,55]) 240 | async def checkRestart() -> None: 241 | db = Database() 242 | onTime,offTime,totalTime = db.getStreamTableValues() 243 | online = StaticMethods.checkOnline(db) 244 | timeSinceRestart = time.time() - globals.botStartTime 245 | timeSinceOffline = time.time() - offTime 246 | if not online and timeSinceRestart > Constants.TIME_BEFORE_BOT_RESTART and timeSinceOffline > Constants.TIME_OFFLINE_BEFORE_RESTART: 247 | StaticMethods.safeRebootServer() 248 | logger.debug("TimeSinceRestart: " + str(timeSinceRestart)) 249 | logger.debug("TimeSinceOffline: " + str(timeSinceOffline)) 250 | 251 | @component.with_schedule 252 | @tanjun.as_time_schedule(minutes=[0,10,20,30,40,50]) 253 | async def presenceGrabber(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 254 | members = rest.fetch_members(Constants.GUILD_ID) 255 | db = Database() 256 | online = StaticMethods.checkOnline(db) 257 | presencesDict = db.getPresenceDay(date.today()) 258 | hourMinute = StaticMethods.getHourMinuteString() 259 | statusCounts = {} 260 | memberCount = 0 261 | if online: 262 | statusCounts["streaming"] = online 263 | async for member in members: 264 | memberCount += 1 265 | presence = member.get_presence() 266 | if presence != None: 267 | status = presence.visible_status 268 | statusStr = str(status) 269 | if statusStr in statusCounts: 270 | statusCounts[statusStr] += 1 271 | else: 272 | statusCounts[statusStr] = 1 273 | statusCounts["members"] = memberCount 274 | presencesDict[hourMinute] = statusCounts 275 | db.setPresenceDay(date.today(), presencesDict) 276 | 277 | @component.with_schedule 278 | @tanjun.as_time_schedule(minutes = [1,11,21,31,41,51]) 279 | async def smartAlert(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 280 | db = Database() 281 | presencesDict = db.getPresenceDay(date.today()) 282 | lastWeekPresenceDict = db.getLastWeeksDayPresenceData(date.today()) 283 | hourMinute = StaticMethods.getHourMinuteString() 284 | lookAheadHourMinute = StaticMethods.getHourMinuteString(offset=Constants.SMART_ALERT_LOOK_AHEAD) 285 | if lastWeekPresenceDict: 286 | maxOnlineLastWeek = StaticMethods.getMaxOnlineInPresenceDict(lastWeekPresenceDict) 287 | if presencesDict[hourMinute] and lastWeekPresenceDict[lookAheadHourMinute]: 288 | lookAheadOnline = lastWeekPresenceDict[lookAheadHourMinute]['online'] 289 | nowOnline = presencesDict[hourMinute]['online'] 290 | onlineThreshold = int(maxOnlineLastWeek * Constants.PERCENTAGE_OF_MAX) 291 | if nowOnline >= onlineThreshold and lookAheadOnline >= onlineThreshold: 292 | StaticMethods.smartRebroadcast() 293 | 294 | @component.with_schedule 295 | @tanjun.as_interval(Constants.CONFESSION_CHECK_TIMER) 296 | async def resetUnreviewedConfessions(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 297 | StaticMethods.resetUnfinishedConfessions() 298 | db = Database() 299 | value = db.getAllUnreviewedConfessions() 300 | if value: 301 | minVal = 99 302 | minAlertsId = 0 303 | for val in value: 304 | if val[0] not in globals.confessionIds: 305 | globals.confessionIds[val[0]] = 1 306 | if minVal > globals.confessionIds[val[0]]: 307 | minVal = globals.confessionIds[val[0]] 308 | minAlertsId = val[0] 309 | alertIntervals = Constants.CONFESSION_ALERT_INTERVALS 310 | minVal = len(alertIntervals)-1 if minVal > len(alertIntervals)-1 else minVal 311 | if StaticMethods.timeToSeconds(globals.confessionIds["alert"]) >= alertIntervals[minVal]: 312 | globals.confessionIds[minAlertsId] += 1 313 | await rest.create_message(channel=Constants.CONFESSTION_CHANNEL_ID, content=f"There are {len(value)} confessions in need of review =)\n Use to review them") 314 | globals.confessionIds["alert"] = time.time() 315 | for k, v in globals.confessionIds.items(): 316 | if v < globals.confessionIds[minAlertsId]: 317 | globals.confessionIds[k] = globals.confessionIds[minAlertsId] 318 | 319 | @component.with_schedule 320 | @tanjun.as_interval(Constants.APPEAL_CHECK_TIMER) 321 | async def resetUnreviewedAppeals(rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 322 | StaticMethods.resetUnfinishedAppeals() 323 | db = Database() 324 | value = db.getAllUnreviewedAppeals() 325 | if value: 326 | minVal = 99 327 | minAlertsId = 0 328 | for val in value: 329 | if val[0] not in globals.appealIds: 330 | globals.appealIds[val[0]] = 1 331 | if minVal > globals.appealIds[val[0]]: 332 | minVal = globals.appealIds[val[0]] 333 | minAlertsId = val[0] 334 | alertIntervals = Constants.APPEAL_ALERT_INTERVALS 335 | minVal = len(alertIntervals)-1 if minVal > len(alertIntervals)-1 else minVal 336 | if StaticMethods.timeToSeconds(globals.appealIds["alert"]) >= alertIntervals[minVal]: 337 | globals.appealIds[minAlertsId] += 1 338 | await rest.create_message(channel=Constants.APPEAL_CHANNEL_ID, content=f"There are {len(value)} appeals in need of review =)\n Use to review them") 339 | globals.appealIds["alert"] = time.time() 340 | for k, v in globals.appealIds.items(): 341 | if v < globals.appealIds[minAlertsId]: 342 | globals.appealIds[k] = globals.appealIds[minAlertsId] -------------------------------------------------------------------------------- /plugins/commands.py: -------------------------------------------------------------------------------- 1 | import hikari.errors 2 | import tanjun 3 | import utils.StaticMethods as StaticMethods 4 | import time 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | from utils.Database import Database 10 | from datetime import datetime 11 | from decorators.Permissions import Permissions 12 | from decorators.CommandLogger import CommandLogger 13 | from datetime import date 14 | from datetime import timedelta 15 | import utils.DataGrapher as DataGrapher 16 | import hikari 17 | import re 18 | import utils.MiruViews as MiruViews 19 | import alluka 20 | import asyncio 21 | from utils.EmbedCreator import EmbedCreator 22 | 23 | component = tanjun.Component() 24 | 25 | @component.with_slash_command 26 | @tanjun.as_slash_command("ban-appeal", "Appeal a ban.", always_defer= True, default_to_ephemeral= True) 27 | @CommandLogger 28 | async def confess(ctx: tanjun.abc.SlashContext) -> None: 29 | view = MiruViews.AppealModalView(autodefer=False) 30 | await ctx.respond("Pre-type your ban appeal and then hit the submit button when you are ready to submit it.\n Button will time out after a few mins, so re-type command if it doesn't work", components=view) 31 | message = await ctx.fetch_last_response() 32 | await view.start(message) 33 | await view.wait() 34 | await ctx.interaction.delete_initial_response() 35 | 36 | @component.with_slash_command 37 | @tanjun.checks.with_check(StaticMethods.isPermission) 38 | @tanjun.as_slash_command("appeal-review", "View appeals that need to be reviewd for approval or denial",default_to_ephemeral= True, always_defer= True) 39 | async def appealReview(ctx: tanjun.abc.SlashContext, rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 40 | db = Database() 41 | appealId,appeal, title = db.getUnreviewedAppeal() 42 | if appealId: 43 | view = MiruViews.AppealReView(appealId=appealId, tanCtx=ctx, appeal=appeal, rest=rest, title=title) 44 | content = f"## {appealId}:{title}\n``` {appeal} ```" 45 | await ctx.respond(content=content, components=view) 46 | message = await ctx.fetch_last_response() 47 | await view.start(message) 48 | await view.wait() 49 | message = await ctx.fetch_last_response() 50 | await ctx.interaction.delete_message(message) 51 | else: 52 | await ctx.respond("There are no appealss in need of review") 53 | 54 | @component.with_slash_command 55 | @tanjun.checks.with_check(StaticMethods.isPermission) 56 | @tanjun.with_str_slash_option("channelid", "Text channel ID you wish to send a message to in order to test permissions") 57 | @tanjun.as_slash_command("test-permission", "Test a notification for a specific platform",default_to_ephemeral= True, always_defer= True) 58 | async def testNotification(ctx: tanjun.abc.SlashContext, rest: alluka.Injected[hikari.impl.RESTClientImpl], channelid:int) -> None: 59 | StaticMethods.logCommand("testNotification", ctx) 60 | messageContent = "Hooray, I can post here! Permissions looking good. Deleting this message after 60 seconds" 61 | embedMaker = EmbedCreator( 62 | f"{Constants.streamerName} is now live on test platform!", 63 | "Test Title", 64 | "https://www.google.com/", 65 | 'images/platformImages/twitchImage.png', 66 | Constants.twitchEmbedColor, 67 | Constants.defaultIcon, 68 | "TestUserName" 69 | ) 70 | task = asyncio.create_task(embedMaker.getEmbed()) 71 | try: 72 | testEmbed = await task 73 | message = await rest.create_message(channel = int(channelid), content = messageContent,embed=testEmbed) 74 | await ctx.respond("Success") 75 | await asyncio.sleep(60) 76 | await message.delete() 77 | except hikari.errors.ForbiddenError: 78 | await ctx.respond("Don't have permissions for this channel. Permissions Needed: View Channel, Post Messages, Embed Links.") 79 | 80 | @component.with_slash_command 81 | @tanjun.checks.with_check(StaticMethods.isPermission) 82 | @tanjun.as_slash_command("confess-review", "View confessions that need to be reviewd for approval or denial",default_to_ephemeral= True, always_defer= True) 83 | async def confessReview(ctx: tanjun.abc.SlashContext, rest: alluka.Injected[hikari.impl.RESTClientImpl]) -> None: 84 | db = Database() 85 | confessionId,confession, title = db.getUnreviewedConfession() 86 | if confessionId: 87 | view = MiruViews.ConfessionReView(confessionId=confessionId, tanCtx=ctx, confession=confession, rest=rest, title=title) 88 | content = f"## {confessionId}:{title}\n``` {confession} ```" 89 | await ctx.respond(content=content, components=view) 90 | message = await ctx.fetch_last_response() 91 | await view.start(message) 92 | await view.wait() 93 | message = await ctx.fetch_last_response() 94 | await ctx.interaction.delete_message(message) 95 | else: 96 | await ctx.respond("There are no confessions in need of review") 97 | 98 | @component.with_slash_command 99 | @tanjun.as_slash_command("confess", "Anonymously post a confession or question to the confessions channel.", always_defer= True, default_to_ephemeral= True) 100 | async def confess(ctx: tanjun.abc.SlashContext) -> None: 101 | view = MiruViews.ConfessionModalView(autodefer=False) 102 | await ctx.respond("Pre-type your Confession/Question and then hit the submit button when you are ready to submit it.\n Button will time out after a few mins, so re-type command if it doesn't work", components=view) 103 | message = await ctx.fetch_last_response() 104 | await view.start(message) 105 | await view.wait() 106 | await ctx.interaction.delete_initial_response() 107 | 108 | @component.with_slash_command 109 | @tanjun.with_bool_slash_option("rerunannounce","True if you want rerun pings False if not") 110 | @tanjun.as_slash_command("announce-rerun-toggle", "Toggle whether or not the bot will announce reruns", default_to_ephemeral=True, always_defer=True) 111 | @Permissions(Constants.whiteListedRoleIDs) 112 | @CommandLogger 113 | async def announceRerunToggle(ctx: tanjun.abc.SlashContext, rerunannounce:bool) -> None: 114 | db = Database() 115 | db.setRerunAnnounce(rerunannounce) 116 | onOff = "ON" if rerunannounce else "OFF" 117 | await ctx.respond(f"Rerun announcements have been turned {onOff}.") 118 | 119 | @component.with_slash_command 120 | @tanjun.with_str_slash_option("title", "The temporary title you wish to add") 121 | @tanjun.with_str_slash_option("platform", "The platform you wish to add a temporary title for") 122 | @tanjun.with_str_slash_option("accountname", "The account name for the platform you wish to create a temp title for. Optional if only 1 account", default="") 123 | @tanjun.as_slash_command("title", "Add a temporary title for a platform", default_to_ephemeral=True, always_defer=True) 124 | @Permissions(Constants.whiteListedRoleIDs) 125 | @CommandLogger 126 | async def tempTitle(ctx: tanjun.abc.SlashContext, title: str, platform: str, accountname: str) -> None: 127 | platforms = ['chaturbate','onlyfans','fansly','twitch','youtube','kick','cam4','mfc','bongacams', 'stripchat'] 128 | db = Database() 129 | if platform.lower() in platforms: 130 | if not accountname: 131 | accounts = db.getPlatformAccountNames(platform) 132 | if len(accounts) == 1: 133 | accountname = accounts[0] 134 | elif len(accounts) == 0: 135 | await ctx.respond("No accounts for this platform in the database. Have you added this account in constants.py?") 136 | else: 137 | await ctx.respond(f"More than one account for this platform. You must input one from this list {accounts}") 138 | if accountname and db.doesAccountExist(platform, accountname): 139 | if title: 140 | db.addTempTitle(title,platform,accountname) 141 | await ctx.respond(f"sucessfully input the new temp title: '{title}'") 142 | else: 143 | await ctx.respond("Entered title is empty") 144 | else: 145 | await ctx.respond("Bad account name given") 146 | else: 147 | await ctx.respond(f"platform name input incorrectly. Use one from this list {platforms}") 148 | 149 | @component.with_slash_command 150 | @tanjun.as_slash_command("stream-stats", f"Get stats on how much {Constants.streamerName} has been streaming.", default_to_ephemeral=True, always_defer=True) 151 | @Permissions(Constants.whiteListedRoleIDs) 152 | @CommandLogger 153 | async def streamStats(ctx: tanjun.abc.SlashContext) -> None: 154 | weekData = StaticMethods.getWeekStreamingMinutes(date.today()) 155 | twoWeekData = StaticMethods.getWeekStreamingMinutes(date.today() - timedelta(days = 7), weekData) 156 | threeWeekData = StaticMethods.getWeekStreamingMinutes(date.today() - timedelta(days = 14), twoWeekData) 157 | fourWeekData = StaticMethods.getWeekStreamingMinutes(date.today() - timedelta(days = 21), threeWeekData) 158 | weekData = StaticMethods.replaceIntsWithString(weekData) 159 | twoWeekData = StaticMethods.replaceIntsWithString(twoWeekData) 160 | threeWeekData = StaticMethods.replaceIntsWithString(threeWeekData) 161 | fourWeekData = StaticMethods.replaceIntsWithString(fourWeekData) 162 | await ctx.respond(f"One Week Totals:{weekData}\nTwo Week Totals:{twoWeekData}\nFour Week Totals:{fourWeekData}\nChecks are made once every 10 min, so figures not exact") 163 | 164 | @component.with_slash_command 165 | @tanjun.with_str_slash_option("days", "Number of days back to include in the graph. Default is 1", default = 1) 166 | @tanjun.with_str_slash_option("inputdate", "Date in yyyy-mm-dd format. If you don't enter anything today's date will be used", default = "") 167 | @tanjun.as_slash_command("users-graph", "get agraph for the active users. Date in yyyy-mm-dd format, or todays date if no input.",default_to_ephemeral= True, always_defer=True) 168 | @Permissions(Constants.whiteListedRoleIDs) 169 | @CommandLogger 170 | async def activeDailyUsersGraph(ctx: tanjun.abc.SlashContext, inputdate: str, days: int) -> None: 171 | if not inputdate: 172 | inputdate = str(date.today()) 173 | restring = r"([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))" 174 | if re.search(restring, inputdate): 175 | db = Database() 176 | days = int(days) 177 | inputDateToDateTime = datetime.strptime(inputdate, '%Y-%m-%d').date() 178 | previousDate = inputDateToDateTime - timedelta(days = days - 1) 179 | isPresDateExists = db.isPresDateExists(inputdate) 180 | if isPresDateExists and db.isPresDateExists(str(previousDate)): 181 | path = DataGrapher.createUserDayGraph(inputdate, days=days) 182 | file = hikari.File(path) 183 | await ctx.respond(file) 184 | else: 185 | errorString =f"There is no data for {inputdate}" if not isPresDateExists else f"There there is not enough data to go back to {previousDate}." 186 | await ctx.respond(errorString) 187 | else: 188 | await ctx.respond("Improper date format, use yyyy-mm-dd") 189 | 190 | @component.with_slash_command 191 | @tanjun.with_bool_slash_option("pingtruefalse", "True sets pings/everyone mentions on, False turns them off") 192 | @tanjun.as_slash_command("ping-toggle", "Toggle for role pings in online announcements", default_to_ephemeral=True, always_defer=True) 193 | @Permissions(Constants.whiteListedRoleIDs) 194 | @CommandLogger 195 | async def togglePing(ctx: tanjun.abc.SlashContext, pingtruefalse: bool) -> None: 196 | db = Database() 197 | db.setPing(pingtruefalse) 198 | onOff = "ON" if pingtruefalse else "OFF" 199 | await ctx.respond(f"Role mention pings have been turned {onOff}.") 200 | 201 | @component.with_slash_command 202 | @tanjun.as_slash_command("shutdown-bot", "Shut down the bot before restarting it so some info can be saved to the database", default_to_ephemeral= True, always_defer= True) 203 | @Permissions(Constants.whiteListedRoleIDs) 204 | @CommandLogger 205 | async def shutDown(ctx: tanjun.abc.SlashContext) -> None: 206 | await ctx.respond("Shutting down the bot saving stuff to database before shtudown") 207 | StaticMethods.setOfflineAddTime() 208 | exit() 209 | 210 | @component.with_slash_command 211 | @tanjun.as_slash_command("image-check-pin", "Check to see if an image is pinned", default_to_ephemeral= True, always_defer= True) 212 | @Permissions(Constants.whiteListedRoleIDs) 213 | @CommandLogger 214 | async def checkPin(ctx: tanjun.abc.SlashContext) -> None: 215 | url = StaticMethods.checkImagePin() 216 | if url: 217 | await ctx.respond(f"{url} is currently pinned") 218 | else: 219 | await ctx.respond("There is currently no image pinned") 220 | 221 | @component.with_slash_command 222 | @tanjun.as_slash_command("image-unpin", "if an image is pinned, it will be unpinned", default_to_ephemeral= True, always_defer= True) 223 | @Permissions(Constants.whiteListedRoleIDs) 224 | @CommandLogger 225 | async def unPinImage(ctx: tanjun.abc.SlashContext) -> None: 226 | await ctx.respond("Unpinning any iamge that may be pinned") 227 | StaticMethods.unPin() 228 | 229 | @component.with_slash_command 230 | @tanjun.with_str_slash_option("imgurl", "Url of the image you wish to be pinned") 231 | @tanjun.with_int_slash_option("hours", "number of hours you wish the image to be pinned for") 232 | @tanjun.as_slash_command("image-pin", "set default embed image for set amount of time in hours", default_to_ephemeral=True, always_defer= True) 233 | @Permissions(Constants.whiteListedRoleIDs) 234 | @CommandLogger 235 | async def pinImage(ctx: tanjun.abc.SlashContext, imgurl: str, hours: int) -> None: 236 | await ctx.respond(f"Pinning {imgurl} for {hours} hours") 237 | StaticMethods.pinImage(imgurl, hours) 238 | 239 | @component.with_slash_command 240 | @tanjun.as_slash_command("rebroadcast", "Resend online notification to your preset discord channel, assuming the streamer is online.", default_to_ephemeral=True, always_defer= True) 241 | @Permissions(Constants.whiteListedRoleIDs) 242 | @CommandLogger 243 | async def rebroadcast(ctx: tanjun.abc.Context) -> None: 244 | await ctx.respond("Online Notifications should be resent when the next online check for each platform is made (could be a few minutes), assuming " + Constants.streamerName + " is online.") 245 | StaticMethods.setRebroadcast() 246 | 247 | @component.with_slash_command 248 | @tanjun.with_str_slash_option("imgurl", "Url of the image you wish to be embedded. Will also be pinned") 249 | @tanjun.as_slash_command("rebroadcast-image", "Rebroadcast with a url, image will be embedded in the new announcement", default_to_ephemeral=True, always_defer= True) 250 | @Permissions(Constants.whiteListedRoleIDs) 251 | @CommandLogger 252 | async def rebroadcastWithImage(ctx: tanjun.abc.SlashContext, imgurl: str) -> None: 253 | await ctx.respond(f"Added {imgurl} to the embed image list and will rebroadcast when the next online check is made. (could be minutes)") 254 | StaticMethods.pinImage(imgurl, Constants.PIN_TIME_SHORT) 255 | StaticMethods.setRebroadcast() 256 | 257 | @component.with_slash_command 258 | @tanjun.as_slash_command("image-list-show", "Show urls of images that are on the list to be embedded", default_to_ephemeral=True, always_defer= True) 259 | @Permissions(Constants.whiteListedRoleIDs) 260 | @CommandLogger 261 | async def showImgList(ctx: tanjun.abc.Context) -> None: 262 | db = Database() 263 | twImgList, twImgQue, bannedImages = db.getTwImgStuff() 264 | await ctx.respond(twImgList) 265 | 266 | @component.with_slash_command 267 | @tanjun.with_str_slash_option("url", "Url of the image you wish to be added to the image embed list") 268 | @tanjun.as_slash_command("image-list-add", "Add an image to the list of images to be embedded", default_to_ephemeral=True, always_defer= True) 269 | @Permissions(Constants.whiteListedRoleIDs) 270 | @CommandLogger 271 | async def addImgList(ctx: tanjun.abc.SlashContext, url: str) -> None: 272 | StaticMethods.addImageListQue(url) 273 | await ctx.respond(f"Added {url} to the embed image list") 274 | 275 | @component.with_slash_command 276 | @tanjun.with_str_slash_option("url", "Url of the image you wish to remove from the image embed list") 277 | @tanjun.as_slash_command("image-list-remove", "Remove an image from the list of images that will be in embedded notifications", default_to_ephemeral=True, always_defer= True) 278 | @Permissions(Constants.whiteListedRoleIDs) 279 | @CommandLogger 280 | async def remImgList(ctx: tanjun.abc.SlashContext, url: str) -> None: 281 | db = Database() 282 | twImgList, twImgQue, bannedList = db.getTwImgStuff() 283 | pinUrl = StaticMethods.checkImagePin() 284 | if pinUrl: 285 | StaticMethods.unPin() 286 | if url in twImgList: 287 | bannedList.append(url) 288 | db.setBannedList(bannedList) 289 | twImgList.remove(url) 290 | await ctx.respond(f"Removed {url} from the embed image list") 291 | db.setTwImgList(twImgList) 292 | if url in twImgQue: 293 | twImgQue.remove(url) 294 | db.setTwImgQueue(twImgQue) 295 | else: 296 | await ctx.respond(f"{url} could not be found in the image embed list. Nothing was removed.") 297 | 298 | @component.with_slash_command 299 | @tanjun.as_slash_command("stream-status", "Find out what " +Constants.streamerName + " is currently doing", default_to_ephemeral=True, always_defer= True) 300 | @CommandLogger 301 | async def streamStatus(ctx: tanjun.abc.Context) -> None: 302 | db = Database() 303 | lastOnline,lastOffline,totalStreamTime = db.getStreamTableValues() 304 | streamingOn = StaticMethods.checkOnline(db) 305 | if not streamingOn: 306 | if lastOffline == 0: 307 | await ctx.respond(Constants.streamerName + " isn't currently streaming , but check out her offline content! \n Links: "+ Constants.linkTreeUrl) 308 | else: 309 | hours, minutes = StaticMethods.timeToHoursMinutes(lastOffline) 310 | await ctx.respond(Constants.streamerName + " isn't currently streaming and has been offline for H:" + str(hours) + " M:" + str(minutes) + ", but check out her offline content! \n Links: "+ Constants.linkTreeUrl) 311 | else: 312 | hours, minutes = StaticMethods.timeToHoursMinutes(lastOnline) 313 | await ctx.respond(Constants.streamerName + " is currently streaming on: \n " + streamingOn + " and has been online for H:" + str(hours) + " M:" + str(minutes) + "\n Links: " + Constants.linkTreeUrl) 314 | tHours, tMinutes = StaticMethods.timeToHoursMinutesTotalTime(totalStreamTime) 315 | date = datetime.fromtimestamp(Constants.RECORD_KEEPING_START_DATE) 316 | await ctx.respond(Constants.streamerName + " has streamed a grand total of H:" + str(tHours) + " M:" + str(tMinutes) + " since records have been kept starting on " + str(date)) 317 | 318 | @component.with_slash_command 319 | @tanjun.with_int_slash_option("epocstart", "The epoc time in seconds when the subathon started", default=0) 320 | @tanjun.as_slash_command("subathon-start", "Start a subathon timer", default_to_ephemeral=True, always_defer= True) 321 | @Permissions(Constants.whiteListedRoleIDs) 322 | @CommandLogger 323 | async def subathon_start(ctx: tanjun.abc.SlashContext, epocstart: int) -> None: 324 | db = Database() 325 | sub = db.getSubathonStatus() 326 | subathon = sub[0] 327 | date = datetime.fromtimestamp(epocstart) 328 | if not subathon: 329 | await ctx.respond("Subathon timer has been set; starting at " + str(date)) 330 | db.startSubathon(epocstart) 331 | else: 332 | await ctx.respond("There's a subathon already running") 333 | 334 | @component.with_slash_command 335 | @tanjun.as_slash_command("subathon-end", "End a subathon timer", default_to_ephemeral=True, always_defer= True) 336 | @Permissions(Constants.whiteListedRoleIDs) 337 | @CommandLogger 338 | async def subathon_end(ctx: tanjun.abc.Context)-> None: 339 | db = Database() 340 | subathon,subStart,subEndDontUse = db.getSubathonStatusClean() 341 | if subathon: 342 | await ctx.respond("Subathon timer has ended") 343 | subEnd = time.time() 344 | db.endSubathon(subEnd) 345 | longestSubLength = db.getSubathonLongest() 346 | currentSubLength = subEnd - subStart 347 | if currentSubLength > longestSubLength: 348 | db.setLongestSubathon(currentSubLength,subStart) 349 | else: 350 | await ctx.respond("There isn't a subathon to end") 351 | 352 | @component.with_slash_command 353 | @tanjun.as_slash_command("subathon", "See subathon status and time online", default_to_ephemeral=True, always_defer= True) 354 | @CommandLogger 355 | async def subathon(ctx: tanjun.abc.Context)-> None: 356 | db = Database() 357 | subathon,subStart,subEnd = db.getSubathonStatusClean() 358 | longestSub, longestSubTime = db.getSubathonLongestTime() 359 | if subathon: 360 | hours, minutes = StaticMethods.timeToHoursMinutes(subStart) 361 | await ctx.respond("There is currently a subathon running that has been running for " + str(hours) + " hours, and " + str(minutes) + " minutes") 362 | elif subEnd > subStart: 363 | hours, minutes = StaticMethods.timeToHoursMinutesStartEnd(subStart, subEnd) 364 | lHours, lMinutes = StaticMethods.timeToHoursMinutesTotalTime(longestSub) 365 | date = datetime.fromtimestamp(longestSubTime) 366 | await ctx.respond("There currently isn't a subathon running but the last one ran for " + str(hours) + " hours, and " + str(minutes) + " minutes") 367 | await ctx.respond("The longest subathon ran for "+ str(lHours) + " hours, and " + str(lMinutes) + " minutes on " + str(date)) 368 | elif subEnd == 0: 369 | await ctx.respond("There isn't a subathon running and a subathon hasn't been completed yet") 370 | 371 | @component.with_slash_command 372 | @tanjun.as_slash_command("reboot", "reboot the bot and its server", default_to_ephemeral=True, always_defer= True) 373 | @Permissions(Constants.whiteListedRoleIDs) 374 | @CommandLogger 375 | async def rebootServer(ctx: tanjun.abc.Context)-> None: 376 | await ctx.respond("rebooting the server") 377 | StaticMethods.rebootServer() 378 | 379 | @tanjun.as_loader 380 | def load(client: tanjun.abc.Client) -> None: 381 | client.add_component(component.copy()) 382 | -------------------------------------------------------------------------------- /plugins/listeners.py: -------------------------------------------------------------------------------- 1 | import hikari 2 | import alluka 3 | import tanjun 4 | try: 5 | from AppConstants import Constants as Constants 6 | except ImportError: 7 | from DefaultConstants import Constants as Constants 8 | from datetime import datetime 9 | import time 10 | 11 | component = tanjun.Component() 12 | 13 | @component.with_listener(hikari.MessageDeleteEvent) 14 | async def printDelete(event: hikari.MessageDeleteEvent, rest: alluka.Injected[hikari.impl.RESTClientImpl]): 15 | try: 16 | file = open("deletedMessageLogs.txt", 'a') 17 | date = datetime.fromtimestamp(time.time()) 18 | file.write(f"{date} Author: {event.old_message.author.id}-{event.old_message.author.username} - deleted: {event.old_message.content} or {event.old_message.embeds} in {event.channel_id} \n") 19 | file.close() 20 | except(AttributeError): 21 | pass 22 | 23 | @tanjun.as_loader 24 | def load(client: tanjun.abc.Client) -> None: 25 | client.add_component(component.copy()) -------------------------------------------------------------------------------- /python.gitignore: -------------------------------------------------------------------------------- 1 | #SQL db 2 | *.db 3 | #secret stuff 4 | secrets/ 5 | token 6 | secrets 7 | test* 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/#use-with-ide 117 | .pdm.toml 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombg/SassBot/5a16a4992b70b2d895a1654a1c95cc2b1138f4df/requirements.txt -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import logging 4 | from pyvirtualdisplay import Display 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | from utils.bot import build_bot 10 | import colorlog 11 | 12 | logger = logging.getLogger(__name__) 13 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 14 | handler = colorlog.StreamHandler() 15 | handler.setFormatter(colorlog.ColoredFormatter( 16 | '%(log_color)s%(bold)s%(levelname)s:%(name)s:%(message)s')) 17 | handlers = [handler] 18 | logging.basicConfig(level=Constants.OTHER_LIBRARIES_LOG_LEVEL, handlers=handlers) 19 | 20 | if os.name != "nt": 21 | import uvloop 22 | uvloop.install() 23 | 24 | if __name__ == "__main__": 25 | if platform.system() == "Linux": 26 | logger.info("Opening Display") 27 | display = Display(visible=0, size=(1080,720)) 28 | display.start() 29 | 30 | build_bot().run() 31 | 32 | if platform.system() == "Linux": 33 | logger.info("Closing Display") 34 | display.stop() 35 | 36 | -------------------------------------------------------------------------------- /utils/DataGrapher.py: -------------------------------------------------------------------------------- 1 | from utils.Database import Database 2 | import matplotlib.pyplot as plt 3 | import os 4 | from datetime import date 5 | try: 6 | from AppConstants import Constants as Constants 7 | except ImportError: 8 | from DefaultConstants import Constants as Constants 9 | from datetime import datetime 10 | from datetime import timedelta 11 | 12 | def createUserDayGraph(inputDate: str, days = 1) -> None: 13 | db = Database() 14 | inputDate = datetime.strptime(inputDate, '%Y-%m-%d').date() 15 | lastWeekPresencesDict = db.getLastWeeksDayPresenceData(inputDate) 16 | presencesDict = {} 17 | for i in reversed(range(days)): 18 | previousDate = inputDate - timedelta(days = i) 19 | previousDateDict = db.getPresenceDay(previousDate) 20 | for k, v in previousDateDict.items(): 21 | key = str(previousDate.day)+"-" + k if days > 1 else k 22 | presencesDict[key] = v 23 | x, yTotalUsers, yDnd, yOnline, yIdle = getTodaysLists(presencesDict) 24 | yTotalUsersLastWeek = getLastWeekList(lastWeekPresencesDict, x) 25 | totalMembers = addTotalMembers(presencesDict) 26 | plt.figure(figsize=(15, 5)) 27 | plt.xticks(rotation='vertical') 28 | if lastWeekPresencesDict and days == 1: 29 | plt.plot(x,yTotalUsersLastWeek, label = "Total users(same day last week)", color = "cyan") 30 | plt.plot(x,yTotalUsers, label = "Total users(logged in to discord)", color = "blue") 31 | plt.plot(x,yDnd, label ="dnd", color = 'red') 32 | plt.plot(x,yOnline, label = "online", color = "green") 33 | plt.plot(x, yIdle, label = "idle", color = "orange") 34 | addOnlineCols(presencesDict) 35 | plt.legend(bbox_to_anchor=(1.075, 1.0), loc='upper left') 36 | xlabel = "Time(D-HH:MM)" if days > 1 else "Time" 37 | plt.xlabel(xlabel) 38 | plt.ylabel("Users") 39 | title = str(inputDate - timedelta(days = days - 1)) + " to " + str(inputDate) if days > 1 else inputDate 40 | plt.title(title) 41 | ax = plt.gca() 42 | hideLabelsAndTicks(ax,days) 43 | plotSecondYAxis(x, totalMembers, ax) 44 | if not os.path.exists("graphs"): 45 | os.makedirs("graphs") 46 | plt.tight_layout() 47 | path = f"graphs/{title}.png" 48 | plt.savefig(path) 49 | return path 50 | 51 | def plotSecondYAxis(x, totalMembers, ax): 52 | ax2 = ax.twinx() 53 | ax2.plot(x,totalMembers, color = "violet", label = "All members(offline and online)") 54 | ax2.set_ylabel("All members", color = "violet") 55 | ax2.legend(bbox_to_anchor=(1.075, 0.4), loc='upper left') 56 | ax2.tick_params(axis='y', labelcolor="violet") 57 | 58 | def hideLabelsAndTicks(ax, days): 59 | labelsToHide = ax.xaxis.get_ticklabels() 60 | ticksToHide = ax.xaxis.get_major_ticks() 61 | stepSize = 6 * days 62 | labelsToHide = list(set(labelsToHide) - set(labelsToHide[::stepSize])) #x[:a:b:c] - list slicing - a is the starting index, b is the ending index and c is the optional step size. 63 | for label in labelsToHide: #L[x::y] means a slice of L where the x is the index to start from and y is the step size. 64 | label.set_visible(False) #temp[::6] means every 6th element from temp. temp = del temp[::6] the same thing? 65 | ticksToHide = list(set(ticksToHide) - set(ticksToHide[::stepSize])) 66 | for tick in ticksToHide: 67 | tick.set_visible(False) 68 | 69 | def addTotalMembers(presencesDict: dict): 70 | totalMembers = [] 71 | for k, v in presencesDict.items(): 72 | if v: 73 | if 'members' in v.keys(): 74 | totalMembers.append(v['members']) 75 | else: 76 | totalMembers.append(None) 77 | else: 78 | totalMembers.append(None) 79 | return totalMembers 80 | 81 | def addOnlineCols(presencesDict): 82 | dictKeys = list(presencesDict) 83 | labels = [] 84 | for k, v in presencesDict.items(): 85 | if v: 86 | if 'streaming' in v.keys(): 87 | try: 88 | newFaceColor = getFaceColor(v['streaming']) 89 | nextkey = dictKeys[dictKeys.index(k) + 1] 90 | if newFaceColor not in labels: 91 | plt.axvspan(k, nextkey, facecolor=newFaceColor, alpha=0.25,zorder=3, label = Constants.streamerName + " Streaming " + v['streaming'] ) 92 | labels.append(newFaceColor) 93 | else: 94 | plt.axvspan(k, nextkey, facecolor=newFaceColor, alpha=0.25,zorder=3) 95 | except (ValueError, IndexError): 96 | pass 97 | 98 | def getFaceColor(streamingValues: str): 99 | faceColor = 'g' 100 | if 'RR' in streamingValues: 101 | faceColor = '#808080' 102 | elif 'Kick' in streamingValues: 103 | faceColor = "g" 104 | elif "OF" in streamingValues: 105 | faceColor = Constants.ofEmbedColor 106 | elif "Fans" in streamingValues: 107 | faceColor = Constants.fansEmbedColor 108 | elif "CB" in streamingValues: 109 | faceColor = Constants.cbEmbedColor 110 | elif "YT" in streamingValues: 111 | faceColor = Constants.ytEmbedColor 112 | elif "Twitch" in streamingValues: 113 | faceColor = Constants.ytEmbedColor 114 | elif "Cam4" in streamingValues: 115 | faceColor = Constants.cam4EmbedColor 116 | elif "MFC" in streamingValues: 117 | faceColor = Constants.mfcEmbedColor 118 | elif "BC" in streamingValues: 119 | faceColor = Constants.bcEmbedColor 120 | elif "SC" in streamingValues: 121 | faceColor = Constants.scEmbedColor 122 | elif "EP" in streamingValues: 123 | faceColor = Constants.epEmbedColor 124 | elif "MV" in streamingValues: 125 | faceColor = Constants.mvEmbedColor 126 | return faceColor 127 | 128 | def getLastWeekList(lastWeekPresencesDict, x): 129 | yTotalUsersLastWeek = [] 130 | if lastWeekPresencesDict: 131 | for k, v in lastWeekPresencesDict.items(): 132 | if v: 133 | if k in x: 134 | totalUsers = 0 135 | for ke, va in v.items(): 136 | if isinstance(va,int) and ke != 'members': 137 | totalUsers += va 138 | yTotalUsersLastWeek.append(totalUsers) 139 | elif k in x: 140 | yTotalUsersLastWeek.append(None) 141 | return yTotalUsersLastWeek 142 | 143 | def getTodaysLists(presencesDict): 144 | x = [] 145 | yTotalUsers = [] 146 | yDnd = [] 147 | yOnline = [] 148 | yIdle = [] 149 | for k, v in presencesDict.items(): 150 | x.append(str(k)) 151 | if v: 152 | totalUsers = 0 153 | dnd = 0 154 | online = 0 155 | idle = 0 156 | for keys, vals in v.items(): 157 | if isinstance(vals,int) and keys != 'members': 158 | totalUsers += vals 159 | if keys == 'dnd': 160 | dnd += vals 161 | elif keys == 'online': 162 | online += vals 163 | elif keys == 'idle': 164 | idle += vals 165 | yTotalUsers.append(int(totalUsers)) 166 | yDnd.append(int(dnd)) 167 | yOnline.append(int(online)) 168 | yIdle.append(int(idle)) 169 | else: 170 | yTotalUsers.append(None) 171 | yDnd.append(None) 172 | yOnline.append(None) 173 | yIdle.append(None) 174 | return x,yTotalUsers,yDnd,yOnline,yIdle -------------------------------------------------------------------------------- /utils/Database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | import time 4 | import datetime 5 | from datetime import date 6 | from datetime import timedelta 7 | import utils.StaticMethods as StaticMethods 8 | import logging 9 | try: 10 | from AppConstants import Constants as Constants 11 | except ImportError: 12 | from DefaultConstants import Constants as Constants 13 | 14 | class Database: 15 | def __init__(self): 16 | self.logger = logging.getLogger(__name__) 17 | self.logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 18 | 19 | def connectCursor(self): 20 | try: 21 | conn = sqlite3.connect("sassBot.db") 22 | cur = conn.cursor() 23 | except sqlite3.OperationalError as e: 24 | try: 25 | f = open("testingForLeak.txt", 'w') 26 | f.close() 27 | except Exception as e: 28 | self.logger.error(e) 29 | if "Too many open files" in str(e): 30 | self.logger.critical("File descriptor leak detected. Rebooting") 31 | StaticMethods.rebootServer() 32 | return conn, cur 33 | 34 | # Confessions & Appeals Table Methods 35 | def createAppealsTable(self): 36 | conn,cur = self.connectCursor() 37 | cur.execute('''CREATE TABLE IF NOT EXISTS appeals 38 | ( 39 | appeal_id INTEGER PRIMARY KEY, 40 | appeal TEXT, 41 | appeal_title TEXT, 42 | appeal_status INTEGER, 43 | appealer_id INTEGER, 44 | appealer_name TEXT, 45 | reviewer_id INTEGER, 46 | reviewer_name TEXT, 47 | date_added INTEGER, 48 | date_reviewed INTEGER 49 | ) 50 | ''') 51 | conn.commit() 52 | cur.close() 53 | conn.close() 54 | 55 | def createConfessionsTable(self): 56 | conn,cur = self.connectCursor() 57 | cur.execute('''CREATE TABLE IF NOT EXISTS confessions 58 | ( 59 | confession_id INTEGER PRIMARY KEY, 60 | confession TEXT, 61 | confession_title TEXT, 62 | review_status INTEGER, 63 | reviewer_id INTEGER, 64 | reviewer_name TEXT, 65 | date_added INTEGER, 66 | date_reviewed INTEGER 67 | ) 68 | ''') 69 | conn.commit() 70 | cur.close() 71 | conn.close() 72 | 73 | def addAppeal(self, appeal:str, appealTitle:str, appealerId: int, appealerName:str) -> None: 74 | self.createAppealsTable() 75 | conn,cur = self.connectCursor() 76 | rowVals = (appeal, appealTitle, appealerId, appealerName, time.time()) 77 | exeString = f'''INSERT INTO appeals (appeal,appeal_title, appealer_id, appealer_name, date_added) VALUES (?,?,?,?,?)''' 78 | cur.execute(exeString,rowVals) 79 | conn.commit() 80 | cur.close() 81 | conn.close() 82 | 83 | def addConfession(self, confession:str, title:str) -> None: 84 | self.createConfessionsTable() 85 | conn,cur = self.connectCursor() 86 | rowVals = (confession, title, time.time()) 87 | exeString = f'''INSERT INTO confessions (confession,confession_title, date_added) VALUES (?,?,?)''' 88 | cur.execute(exeString,rowVals) 89 | conn.commit() 90 | cur.close() 91 | conn.close() 92 | 93 | def getUnreviewedConfession(self): 94 | self.createConfessionsTable() 95 | confessionId = 0 96 | confession = "" 97 | title = "" 98 | conn,cur = self.connectCursor() 99 | exeString = '''SELECT confession_id, confession,confession_title FROM confessions WHERE date_reviewed IS NULL LIMIT 1''' 100 | cur.execute(exeString) 101 | values = cur.fetchall() 102 | if values: 103 | confessionId = values[0][0] 104 | confession = values[0][1] 105 | title = values[0][2] 106 | cur.close() 107 | conn.close() 108 | self.setConfessionDateReviewed(confessionId) 109 | return confessionId, confession, title 110 | 111 | def getUnreviewedAppeal(self): 112 | self.createAppealsTable() 113 | appealId = 0 114 | appeal = "" 115 | title = "" 116 | conn,cur = self.connectCursor() 117 | exeString = '''SELECT appeal_id, appeal,appeal_title FROM appeals WHERE date_reviewed IS NULL LIMIT 1''' 118 | cur.execute(exeString) 119 | values = cur.fetchall() 120 | if values: 121 | appealId = values[0][0] 122 | appeal = values[0][1] 123 | title = values[0][2] 124 | cur.close() 125 | conn.close() 126 | self.setAppealDateReviewed(appealId) 127 | return appealId, appeal, title 128 | 129 | def setConfessionDateReviewed(self, confessionId): 130 | self.createConfessionsTable() 131 | conn, cur = self.connectCursor() 132 | exeString = f'''UPDATE confessions SET date_reviewed={time.time()} WHERE confession_id={confessionId} ''' 133 | cur.execute(exeString) 134 | conn.commit() 135 | cur.close() 136 | conn.close() 137 | 138 | def setAppealDateReviewed(self, appealId): 139 | self.createAppealsTable() 140 | conn, cur = self.connectCursor() 141 | exeString = f'''UPDATE appeals SET date_reviewed={time.time()} WHERE appeal_id={appealId} ''' 142 | cur.execute(exeString) 143 | conn.commit() 144 | cur.close() 145 | conn.close() 146 | 147 | def reviewConfession(self, confessionId: int, approveDeny: int, reviewerId: int, reviewerName: str): 148 | self.createConfessionsTable() 149 | conn, cur = self.connectCursor() 150 | values = (approveDeny, reviewerId, reviewerName, time.time(), confessionId) 151 | exeString = f'''UPDATE confessions SET review_status=?, reviewer_id=?, reviewer_name=?, date_reviewed=? WHERE confession_id=? ''' 152 | cur.execute(exeString,values) 153 | conn.commit() 154 | cur.close() 155 | conn.close() 156 | 157 | def reviewAppeal(self, appealId: int, approveDeny: int, reviewerId: int, reviewerName: str): 158 | self.createAppealsTable() 159 | conn, cur = self.connectCursor() 160 | values = (approveDeny, reviewerId, reviewerName, time.time(), appealId) 161 | exeString = f'''UPDATE appeals SET appeal_status=?, reviewer_id=?, reviewer_name=?, date_reviewed=? WHERE appeal_id=? ''' 162 | cur.execute(exeString,values) 163 | conn.commit() 164 | cur.close() 165 | conn.close() 166 | 167 | def getUnfinishedConfessionReviews(self): 168 | self.createConfessionsTable() 169 | conn, cur = self.connectCursor() 170 | exeString = f'''SELECT confession_id, date_reviewed FROM confessions WHERE review_status IS NULL AND date_reviewed IS NOT NULL ''' 171 | cur.execute(exeString) 172 | value = cur.fetchall() 173 | cur.close() 174 | conn.close() 175 | return value 176 | 177 | def getUnfinishedAppealReviews(self): 178 | self.createAppealsTable() 179 | conn, cur = self.connectCursor() 180 | exeString = f'''SELECT appeal_id, date_reviewed FROM appeals WHERE appeal_status IS NULL AND date_reviewed IS NOT NULL ''' 181 | cur.execute(exeString) 182 | value = cur.fetchall() 183 | cur.close() 184 | conn.close() 185 | return value 186 | 187 | def resetAppealDateReviewed(self, appealId): 188 | self.createAppealsTable() 189 | conn, cur = self.connectCursor() 190 | exeString = f'''UPDATE appeals SET date_reviewed=NULL WHERE appeal_id={appealId} ''' 191 | cur.execute(exeString) 192 | conn.commit() 193 | cur.close() 194 | conn.close() 195 | 196 | def resetConfessionDateReviewed(self, confessionId): 197 | self.createConfessionsTable() 198 | conn, cur = self.connectCursor() 199 | exeString = f'''UPDATE confessions SET date_reviewed=NULL WHERE confession_id={confessionId} ''' 200 | cur.execute(exeString) 201 | conn.commit() 202 | cur.close() 203 | conn.close() 204 | 205 | def getAllUnreviewedConfessions(self): 206 | self.createConfessionsTable() 207 | conn,cur = self.connectCursor() 208 | exeString = '''SELECT confession_id, confession,confession_title FROM confessions WHERE date_reviewed IS NULL''' 209 | cur.execute(exeString) 210 | values = cur.fetchall() 211 | cur.close() 212 | conn.close() 213 | return values 214 | 215 | def getAllUnreviewedAppeals(self): 216 | self.createAppealsTable() 217 | conn,cur = self.connectCursor() 218 | exeString = '''SELECT appeal_id, appeal, appeal_title FROM appeals WHERE date_reviewed IS NULL''' 219 | cur.execute(exeString) 220 | values = cur.fetchall() 221 | cur.close() 222 | conn.close() 223 | return values 224 | 225 | # Platform_Accounts Table Methods 226 | def getPlatformTempTitle(self,platform, accountName): 227 | self.checkAddTitleCols() 228 | title = "" 229 | titleTime = 0 230 | conn,cur = self.connectCursor() 231 | exeString = f'''SELECT temp_title, temp_title_time FROM platform_accounts WHERE platform_name='{platform}' AND account_name='{accountName}' ''' 232 | cur.execute(exeString) 233 | values = cur.fetchall() 234 | if values: 235 | title = values[0][0] 236 | titleTime = values[0][1] 237 | cur.close() 238 | conn.close() 239 | return title, titleTime 240 | 241 | def getPlatformAccountNames(self,platform:str): 242 | conn,cur = self.connectCursor() 243 | names= [] 244 | exeString = f'''SELECT account_name FROM platform_accounts WHERE platform_name='{platform}' ''' 245 | if self.isExists(exeString): 246 | cur.execute(exeString) 247 | namesList = cur.fetchall() 248 | for name in namesList: 249 | names.append(name[0]) 250 | cur.close() 251 | conn.close() 252 | return names 253 | 254 | def doesAccountExist(self,platform, accountName): 255 | isExistString = f'''SELECT temp_title, temp_title_time FROM platform_accounts WHERE platform_name='{platform}' AND account_name='{accountName}' ''' 256 | accountExist = self.isExists(isExistString) 257 | return accountExist 258 | 259 | def addTempTitle(self,title: str, platform: str, accountName: str ) -> None: 260 | self.createPlatformAccountsTable() 261 | self.checkAddTitleCols() 262 | conn,cur = self.connectCursor() 263 | if self.doesAccountExist(platform, accountName): 264 | exeString = f'''UPDATE platform_accounts SET temp_title='{title}', temp_title_time={time.time()} WHERE platform_name='{platform}' AND account_name='{accountName}' ''' 265 | cur.execute(exeString) 266 | conn.commit() 267 | cur.close() 268 | conn.close() 269 | else: 270 | self.logger.error("given bad account or platform. can't update title") 271 | 272 | def checkAddTitleCols(self): 273 | isTitleExist = self.isColExist("platform_accounts","temp_title") 274 | if not isTitleExist: 275 | conn,cur = self.connectCursor() 276 | cur.execute('''ALTER TABLE platform_accounts ADD temp_title TEXT ''') 277 | cur.execute('''ALTER TABLE platform_accounts ADD temp_title_time REAL ''') 278 | conn.commit() 279 | cur.close() 280 | conn.close() 281 | 282 | def createPlatformAccountsTable(self) -> None: 283 | conn,cur = self.connectCursor() 284 | cur.execute('''CREATE TABLE IF NOT EXISTS platform_accounts 285 | ( 286 | account_name TEXT NOT NULL, 287 | platform_name TEXT NOT NULL, 288 | last_online_message REAL, 289 | last_stream_start_time REAL, 290 | last_stream_end_time REAL, 291 | temp_title TEXT, 292 | temp_title_time REAL, 293 | PRIMARY KEY (account_name, platform_name), 294 | FOREIGN KEY(platform_name) REFERENCES platforms(platform_name) 295 | ) 296 | ''') 297 | conn.commit() 298 | cur.close() 299 | conn.close() 300 | 301 | def createNewPlatformAccount(self, accountName:str, platformName:str,lastOnlineMessage = 0, lastStreamStartTime = 0, lastStreamEndTime = 0) -> None: 302 | self.createPlatformAccountsTable() 303 | conn,cur = self.connectCursor() 304 | rowVals =(accountName, platformName, lastOnlineMessage, lastStreamStartTime, lastStreamEndTime) 305 | cur.execute('INSERT INTO platform_accounts (account_name, platform_name, last_online_message, last_stream_start_time, last_stream_end_time) VALUES (?,?,?,?,?)',rowVals) 306 | conn.commit() 307 | cur.close() 308 | conn.close() 309 | 310 | def getPlatformAccountsRowValues(self, platformName, accountName): 311 | self.createPlatformAccountsTable() 312 | conn,cur = self.connectCursor() 313 | exeString = f'''SELECT last_online_message,last_stream_start_time,last_stream_end_time FROM platform_accounts WHERE platform_name='{platformName}' AND account_name='{accountName}' ''' 314 | if not self.isExists(exeString): 315 | self.createNewPlatformAccount(accountName, platformName) 316 | cur.execute(exeString) 317 | value = cur.fetchall() 318 | cur.close() 319 | conn.close() 320 | return value[0][0],value[0][1],value[0][2] 321 | 322 | def updatePlatformAccountRowCol(self,platformName,accountName,col,newValue): 323 | self.createPlatformAccountsTable() 324 | conn,cur = self.connectCursor() 325 | exeString = f'''UPDATE platform_accounts SET {col}={newValue} WHERE platform_name='{platformName}' AND account_name='{accountName}' ''' 326 | cur.execute(exeString) 327 | conn.commit() 328 | cur.close() 329 | conn.close() 330 | 331 | # Platform Table Methods 332 | def setRerun(self, isRerun, platform): 333 | self.checkAddRerunCols() 334 | conn,cur = self.connectCursor() 335 | tFalse = 1 if isRerun else 0 336 | exeString = f'''UPDATE platforms SET rerun_playing={tFalse} WHERE platform_name='{platform}' ''' 337 | cur.execute(exeString) 338 | conn.commit() 339 | cur.close() 340 | conn.close() 341 | 342 | def getRerun(self, platform): 343 | isRerun = 0 344 | self.checkAddRerunCols() 345 | conn, cur = self.connectCursor() 346 | exeString = f'''SELECT rerun_playing FROM platforms WHERE platform_name='{platform}' ''' 347 | cur.execute(exeString) 348 | value = cur.fetchall() 349 | cur.close() 350 | conn.close() 351 | if value[0][0] != None: 352 | isRerun = value[0][0] 353 | return isRerun 354 | 355 | def updatePlatformRowCol(self,rowKey,col,newValue): 356 | conn,cur = self.connectCursor() 357 | exeString = f'''UPDATE platforms SET {col}={newValue} WHERE platform_name='{rowKey}' ''' 358 | cur.execute(exeString) 359 | conn.commit() 360 | cur.close() 361 | conn.close() 362 | 363 | def getPlatformsRowCol(self, rowKey, col): 364 | conn,cur = self.connectCursor() 365 | exeString = f'''SELECT {col} FROM platforms WHERE platform_name='{rowKey}' ''' 366 | cur.execute(exeString) 367 | value = cur.fetchall() 368 | cur.close() 369 | conn.close() 370 | return value[0][0] 371 | 372 | def getPlatformsRowValues(self, platformName): 373 | self.checkAddPlatformRow(platformName) 374 | conn,cur = self.connectCursor() 375 | exeString = f'''SELECT last_online_message,last_stream_start_time,last_stream_end_time FROM platforms WHERE platform_name='{platformName}' ''' 376 | cur.execute(exeString) 377 | value = cur.fetchall() 378 | cur.close() 379 | conn.close() 380 | isRerun = self.getRerun(platformName) 381 | return value[0][0],value[0][1],value[0][2], isRerun 382 | 383 | def checkAddPlatformRow(self,platformName): 384 | exeString = f'''SELECT * FROM platforms WHERE platform_name='{platformName}' ''' 385 | isExists = self.isExists(exeString) 386 | if not isExists: 387 | conn,cur = self.connectCursor() 388 | rowVals =(platformName,0,0,0) 389 | cur.execute('INSERT INTO platforms (platform_name, last_online_message,last_stream_start_time,last_stream_end_time) VALUES (?,?,?,?)',rowVals) 390 | conn.commit() 391 | cur.close() 392 | conn.close() 393 | 394 | # Subathon Table Methods 395 | def startSubathon(self,epochTime): 396 | conn,cur = self.connectCursor() 397 | subTrue = 1 398 | exeString = f'''UPDATE subathon SET subathon={subTrue}, start_time={epochTime} ''' 399 | cur.execute(exeString) 400 | conn.commit() 401 | cur.close() 402 | conn.close() 403 | 404 | def endSubathon(self,epochTime): 405 | conn,cur = self.connectCursor() 406 | subFalse = 0 407 | exeString = f'''UPDATE subathon SET subathon={subFalse}, end_time={epochTime} ''' 408 | cur.execute(exeString) 409 | conn.commit() 410 | cur.close() 411 | conn.close() 412 | 413 | def getSubathonStatus(self): 414 | conn,cur = self.connectCursor() 415 | exeString = f'''SELECT subathon,start_time,end_time FROM subathon''' 416 | cur.execute(exeString) 417 | value = cur.fetchall() 418 | cur.close() 419 | conn.close() 420 | return value[0] 421 | 422 | def getSubathonStatusClean(self): 423 | sub = self.getSubathonStatus() 424 | subathon = sub[0] 425 | subStart = sub[1] 426 | subEnd = sub[2] 427 | return subathon,subStart,subEnd 428 | 429 | def getSubathonLongest(self): 430 | conn,cur = self.connectCursor() 431 | exeString = f'''SELECT longest_subathon FROM subathon''' 432 | cur.execute(exeString) 433 | value = cur.fetchall() 434 | cur.close() 435 | conn.close() 436 | return value[0][0] 437 | 438 | def getSubathonLongestTime(self): 439 | conn,cur = self.connectCursor() 440 | exeString = f'''SELECT longest_subathon,longest_subathon_time FROM subathon''' 441 | cur.execute(exeString) 442 | value = cur.fetchall() 443 | cur.close() 444 | conn.close() 445 | return value[0][0],value[0][1] 446 | 447 | def setLongestSubathon(self,subathonLength,subathonStartTime): 448 | conn,cur = self.connectCursor() 449 | exeString = f'''UPDATE subathon SET longest_subathon={subathonLength},longest_subathon_time={subathonStartTime} ''' 450 | cur.execute(exeString) 451 | conn.commit() 452 | cur.close() 453 | conn.close() 454 | 455 | # Stream Table Methods 456 | def setRerunAnnounce(self, isAnnounce): 457 | self.checkAddRerunCols() 458 | conn,cur = self.connectCursor() 459 | tFalse = 1 if isAnnounce else 0 460 | exeString = f'''UPDATE stream SET rerun_ping={tFalse}''' 461 | cur.execute(exeString) 462 | conn.commit() 463 | cur.close() 464 | conn.close() 465 | 466 | def getRerunAnnounce(self): 467 | isRerun = 0 468 | self.checkAddRerunCols() 469 | conn, cur = self.connectCursor() 470 | exeString = f'''SELECT rerun_ping FROM stream ''' 471 | cur.execute(exeString) 472 | value = cur.fetchall() 473 | cur.close() 474 | conn.close() 475 | if value[0][0] != None: 476 | isRerun = value[0][0] 477 | return isRerun 478 | 479 | def getStreamTableValues(self): 480 | conn,cur = self.connectCursor() 481 | exeString = f'''SELECT last_online,last_offline,last_stream_length FROM stream''' 482 | cur.execute(exeString) 483 | value = cur.fetchall() 484 | cur.close() 485 | conn.close() 486 | return value[0][0],value[0][1],value[0][2] 487 | 488 | def setStreamLastOnline(self,lastOnline): 489 | conn,cur = self.connectCursor() 490 | exeString = f'''UPDATE stream SET last_online={lastOnline} ''' 491 | cur.execute(exeString) 492 | conn.commit() 493 | cur.close() 494 | conn.close() 495 | 496 | def setStreamLastOffline(self,lastOffline): 497 | conn,cur = self.connectCursor() 498 | exeString = f'''UPDATE stream SET last_offline={lastOffline} ''' 499 | cur.execute(exeString) 500 | conn.commit() 501 | cur.close() 502 | conn.close() 503 | 504 | def setStreamLastStreamLength(self,lastStreamLength): 505 | conn,cur = self.connectCursor() 506 | exeString = f'''UPDATE stream SET last_stream_length={lastStreamLength} ''' 507 | cur.execute(exeString) 508 | conn.commit() 509 | cur.close() 510 | conn.close() 511 | 512 | def getTwImgStuff(self): 513 | conn,cur = self.connectCursor() 514 | exeString = f'''SELECT tw_img_list,tw_img_queue, img_banned_list FROM stream''' 515 | cur.execute(exeString) 516 | value = cur.fetchall() 517 | if value[0][2] is None: 518 | bannedList = [] 519 | else: 520 | bannedList = json.loads(value[0][2]) 521 | if value[0][0] is None or value[0][1] is None: 522 | twImgList = [] 523 | twImgQue =[] 524 | else: 525 | twImgList = json.loads(value[0][0]) 526 | twImgQue = json.loads(value[0][1]) 527 | cur.close() 528 | conn.close() 529 | return twImgList, twImgQue, bannedList 530 | 531 | def setBannedList(self, bannedList): 532 | conn,cur = self.connectCursor() 533 | bannedListDump = json.dumps(bannedList) 534 | exeString = f'''UPDATE stream SET img_banned_list='{bannedListDump}' ''' 535 | cur.execute(exeString) 536 | conn.commit() 537 | cur.close() 538 | conn.close() 539 | 540 | def setTwImgList(self,twImgList): 541 | conn,cur = self.connectCursor() 542 | twImgListDump = json.dumps(twImgList) 543 | exeString = f'''UPDATE stream SET tw_img_list='{twImgListDump}' ''' 544 | cur.execute(exeString) 545 | conn.commit() 546 | cur.close() 547 | conn.close() 548 | 549 | def setTwImgQueue(self,twImgQueue): 550 | conn,cur = self.connectCursor() 551 | twImgQueueDump = json.dumps(twImgQueue) 552 | exeString = f'''UPDATE stream SET tw_img_queue='{twImgQueueDump}' ''' 553 | cur.execute(exeString) 554 | conn.commit() 555 | cur.close() 556 | conn.close() 557 | 558 | def setImgPin(self, epochTime: int, url: str) -> None: 559 | conn,cur = self.connectCursor() 560 | exeString = f'''UPDATE stream SET img_pin={epochTime}, img_pin_url='{url}' ''' 561 | cur.execute(exeString) 562 | conn.commit() 563 | cur.close() 564 | conn.close() 565 | 566 | def getImgPin(self): 567 | conn,cur = self.connectCursor() 568 | exeString = f'''SELECT img_pin, img_pin_url FROM stream ''' 569 | cur.execute(exeString) 570 | value = cur.fetchall() 571 | cur.close() 572 | conn.close() 573 | return value[0][0],value[0][1] 574 | 575 | def getPing(self): 576 | conn,cur = self.connectCursor() 577 | exeString = f'''SELECT everyone_ping FROM stream ''' 578 | cur.execute(exeString) 579 | value = cur.fetchall() 580 | ping = False 581 | if value[0][0]: 582 | ping = True 583 | cur.close() 584 | conn.close() 585 | return ping 586 | 587 | def setPing(self, ifPing: bool) -> None: 588 | conn,cur = self.connectCursor() 589 | ping = 1 if ifPing else 0 590 | exeString = f'''UPDATE stream SET everyone_ping={ping} ''' 591 | cur.execute(exeString) 592 | conn.commit() 593 | cur.close() 594 | conn.close() 595 | 596 | # User_Presence_Stats Table Methods 597 | def isPresDateExists(self, dataDate: date) -> bool: 598 | exeString = f'''SELECT user_presences FROM user_presence_stats WHERE date='{dataDate}' ''' 599 | isExists = self.isExists(exeString) 600 | return isExists 601 | 602 | def getPresenceDay(self,dataDate: date) -> dict: 603 | conn,cur = self.connectCursor() 604 | presenceDict = {} 605 | exeString = f'''SELECT user_presences FROM user_presence_stats WHERE date='{dataDate}' ''' 606 | if self.isExists(exeString): 607 | cur.execute(exeString) 608 | value = cur.fetchall() 609 | presenceDict = json.loads(value[0][0]) 610 | elif str(dataDate) == str(date.today()): 611 | for i in range(144): 612 | hour = int(i/6) 613 | minute = i%6 614 | minute = minute * 10 615 | hourMinute = f'{hour}:{minute}' 616 | presenceDict[hourMinute] = 0 617 | self.setNewPresenceDay(date.today(),datetime.datetime.now().weekday(),presenceDict) 618 | cur.close() 619 | conn.close() 620 | return presenceDict 621 | 622 | def setPresenceDay(self, dataDate: date, presenceDict: dict) -> None: 623 | conn,cur = self.connectCursor() 624 | presenceDictDump = json.dumps(presenceDict) 625 | exeString = f'''UPDATE user_presence_stats SET user_presences='{presenceDictDump}' WHERE date = '{dataDate}' ''' 626 | cur.execute(exeString) 627 | conn.commit() 628 | cur.close() 629 | conn.close() 630 | 631 | def setNewPresenceDay(self, dataDate: date, dataWeekDay: int, presenceDict: dict) -> None: 632 | conn,cur = self.connectCursor() 633 | presenceDictDump = json.dumps(presenceDict) 634 | rowVals =(dataDate, dataWeekDay, presenceDictDump) 635 | cur.execute('INSERT INTO user_presence_stats (date, week_day, user_presences) VALUES (?,?,?)',rowVals) 636 | conn.commit() 637 | cur.close() 638 | conn.close() 639 | 640 | def getLastWeeksDayPresenceData(self, dayDate: date) -> dict: 641 | conn,cur = self.connectCursor() 642 | previousWeekDayDate = dayDate - timedelta(days = 7) 643 | value = 0 644 | lastWeekPres = self.getPresenceDay(previousWeekDayDate) 645 | if lastWeekPres: 646 | value = lastWeekPres 647 | cur.close() 648 | conn.close() 649 | return value 650 | 651 | # Helper Functions 652 | def isExists(self,query: str) -> bool: 653 | conn,cur = self.connectCursor() 654 | exists = f"SELECT EXISTS({query})" 655 | isExists = False 656 | try: 657 | cur.execute(exists) 658 | value = cur.fetchall() 659 | isExists = value[0][0] 660 | except sqlite3.OperationalError: 661 | self.logger.warning("error when checking if col exists, perhaps no data yet") 662 | cur.close() 663 | conn.close() 664 | return isExists 665 | 666 | def isColExist(self,tableName, colName): 667 | isExist = False 668 | conn,cur = self.connectCursor() 669 | cur.execute(f'PRAGMA table_info({tableName})') 670 | retCols = cur.fetchall() 671 | for col in retCols: 672 | if colName in col: 673 | isExist = True 674 | cur.close() 675 | conn.close() 676 | return isExist 677 | 678 | def checkAddRerunCols(self): 679 | isTitleExist = self.isColExist("stream","rerun_ping") 680 | if not isTitleExist: 681 | conn,cur = self.connectCursor() 682 | cur.execute('''ALTER TABLE stream ADD rerun_ping INTEGER ''') 683 | cur.execute('''ALTER TABLE platforms ADD rerun_playing INTEGER ''') 684 | conn.commit() 685 | cur.close() 686 | conn.close() -------------------------------------------------------------------------------- /utils/EmbedCreator.py: -------------------------------------------------------------------------------- 1 | import hikari 2 | import asyncio 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | import utils.StaticMethods as StaticMethods 8 | 9 | class EmbedCreator: 10 | def __init__(self,description,title,url,thumbnail,color,icon,username,largeThumbnail = "") -> None: 11 | self.title = title 12 | self.description = description 13 | self.url = url 14 | self.thumbnail = thumbnail 15 | self.color = color 16 | self.largeThumbnail = largeThumbnail 17 | self.icon = icon 18 | self.username = username 19 | 20 | async def getEmbed(self): 21 | thumbnailImage = await self.getThumbnailImage() 22 | embed = ( 23 | hikari.Embed( 24 | title = self.title , 25 | description = self.description, 26 | url = self.url, 27 | color = self.color 28 | ).set_image(thumbnailImage) 29 | .set_thumbnail(self.thumbnail) 30 | .set_author(name = self.username, url = self.url, icon = self.icon) 31 | ) 32 | return embed 33 | 34 | async def getThumbnailImage(self): 35 | pinUrl = StaticMethods.checkImagePin() 36 | if self.largeThumbnail and not pinUrl: 37 | embedImg = self.largeThumbnail 38 | else: 39 | embedImg = StaticMethods.getEmbedImage() 40 | return embedImg -------------------------------------------------------------------------------- /utils/MiruViews.py: -------------------------------------------------------------------------------- 1 | import miru 2 | import hikari 3 | from utils.Database import Database 4 | from datetime import timedelta 5 | import plugins.commands as commands 6 | try: 7 | from AppConstants import Constants as Constants 8 | except ImportError: 9 | from DefaultConstants import Constants as Constants 10 | import tanjun 11 | import random 12 | 13 | class ConfessionReView(miru.View): 14 | def __init__(self, *, timeout: float | int | timedelta | None = 120, autodefer: bool = True, confessionId, tanCtx: tanjun.abc.SlashContext, confession:str,rest: hikari.impl.RESTClientImpl,title) -> None: 15 | self.db = Database() 16 | self.confessionId = confessionId 17 | self.tanCtx = tanCtx 18 | self.confession = confession 19 | self.rest = rest 20 | self.title = title 21 | super().__init__(timeout=timeout, autodefer=autodefer) 22 | @miru.button(label="Approve&Finish", style=hikari.ButtonStyle.SUCCESS) 23 | async def approveButton(self, button: miru.Button, ctx: miru.ViewContext) -> None: 24 | self.db.reviewConfession(self.confessionId,1,ctx.member.id,ctx.member.display_name) 25 | content = f"## {self.confessionId}:{self.title}\n``` {self.confession} ```" 26 | await self.rest.create_message(channel=Constants.CONFESSTION_CHANNEL_ID, content=content) 27 | self.stop() 28 | @miru.button(label="Approve&Next", style=hikari.ButtonStyle.SUCCESS) 29 | async def approveAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 30 | self.db.reviewConfession(self.confessionId,1,ctx.member.id,ctx.member.display_name) 31 | content = f"## {self.confessionId}:{self.title}\n``` {self.confession} ```" 32 | await self.rest.create_message(channel=Constants.CONFESSTION_CHANNEL_ID, content=content) 33 | self.stop() 34 | await commands.confessReview(self.tanCtx, self.rest) 35 | @miru.button(label="Pass&Next", style=hikari.ButtonStyle.SECONDARY) 36 | async def passAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 37 | self.stop() 38 | await commands.confessReview(self.tanCtx, self.rest) 39 | @miru.button(label="Deny&Finish", style=hikari.ButtonStyle.DANGER) 40 | async def denyButton(self, button: miru.Button, ctx: miru.ViewContext) -> None: 41 | self.db.reviewConfession(self.confessionId,0,ctx.member.id,ctx.member.display_name) 42 | self.stop() 43 | @miru.button(label="Deny&Next", style=hikari.ButtonStyle.DANGER) 44 | async def denyAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 45 | self.db.reviewConfession(self.confessionId,0,ctx.member.id,ctx.member.display_name) 46 | self.stop() 47 | await commands.confessReview(self.tanCtx, self.rest) 48 | class AppealReView(miru.View): 49 | def __init__(self, *, timeout: float | int | timedelta | None = 120, autodefer: bool = True, appealId, tanCtx: tanjun.abc.SlashContext, appeal:str,rest: hikari.impl.RESTClientImpl,title) -> None: 50 | self.db = Database() 51 | self.appealId = appealId 52 | self.tanCtx = tanCtx 53 | self.appeal = appeal 54 | self.rest = rest 55 | self.title = title 56 | super().__init__(timeout=timeout, autodefer=autodefer) 57 | @miru.button(label="Approve&Finish", style=hikari.ButtonStyle.SUCCESS) 58 | async def approveButton(self, button: miru.Button, ctx: miru.ViewContext) -> None: 59 | self.db.reviewAppeal(self.appealId,1,ctx.member.id,ctx.member.display_name) 60 | content = f"## {self.appealId}:{self.title}\n``` {self.appeal} ```\n<@&{Constants.MOD_ROLE_ID}> Ban appeal approved. Please react to this message to show unban action has been carried out" 61 | await self.rest.create_message(channel=Constants.APPEAL_CHANNEL_ID, content=content, role_mentions=True) 62 | self.stop() 63 | @miru.button(label="Approve&Next", style=hikari.ButtonStyle.SUCCESS) 64 | async def approveAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 65 | self.db.reviewAppeal(self.appealId,1,ctx.member.id,ctx.member.display_name) 66 | content = f"## {self.appealId}:{self.title}\n``` {self.appeal} ```\n<@&{Constants.MOD_ROLE_ID}> Ban appeal approved. Please react to this message to show unban action has been carried out" 67 | await self.rest.create_message(channel=Constants.APPEAL_CHANNEL_ID, content=content, role_mentions=True) 68 | self.stop() 69 | await commands.appealReview(self.tanCtx, self.rest) 70 | @miru.button(label="Pass&Next", style=hikari.ButtonStyle.SECONDARY) 71 | async def passAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 72 | self.stop() 73 | await commands.appealReview(self.tanCtx, self.rest) 74 | @miru.button(label="Deny&Finish", style=hikari.ButtonStyle.DANGER) 75 | async def denyButton(self, button: miru.Button, ctx: miru.ViewContext) -> None: 76 | self.db.reviewAppeal(self.appealId,0,ctx.member.id,ctx.member.display_name) 77 | self.stop() 78 | @miru.button(label="Deny&Next", style=hikari.ButtonStyle.DANGER) 79 | async def denyAndReview(self, button: miru.Button, ctx: miru.ViewContext) -> None: 80 | self.db.reviewAppeal(self.appealId,0,ctx.member.id,ctx.member.display_name) 81 | self.stop() 82 | await commands.appealReview(self.tanCtx, self.rest) 83 | 84 | class ConfessionModal(miru.Modal): 85 | confTitle = miru.TextInput(label="Title", placeholder="Enter your title!", required=True,max_length=75) 86 | confess = miru.TextInput(label="Confession/Question",placeholder="Enter your Confession/Question here", style=hikari.TextInputStyle.PARAGRAPH, required=True, max_length=1900) 87 | async def callback(self, ctx: miru.ModalContext) -> None: 88 | db = Database() 89 | db.addConfession(self.confess.value, self.confTitle.value) 90 | await ctx.respond(f"Submitted Confession: \n```{self.confess.value}```\n This will be posted once it's reveiwed by a mod", flags=hikari.MessageFlag.EPHEMERAL) 91 | 92 | class AppealModal(miru.Modal): 93 | appealTitle = miru.TextInput(label="Username", placeholder="Enter username and platform you wish to be unbanned on", required=True,max_length=75) 94 | appeal = miru.TextInput(label="Ban Appeal",placeholder="Enter your ban appeal here", style=hikari.TextInputStyle.PARAGRAPH, required=True, max_length=1900) 95 | async def callback(self, ctx: miru.ModalContext) -> None: 96 | db = Database() 97 | db.addAppeal(self.appeal.value, self.appealTitle.value, ctx.member.id, ctx.member.display_name) 98 | await ctx.respond(f"Submitted Ban Appeal: \n```{self.appeal.value}```\n This will likely be reviewed on stream for content", flags=hikari.MessageFlag.EPHEMERAL) 99 | 100 | class ConfessionModalView(miru.View): 101 | @miru.button(label="Click to Submit", style=hikari.ButtonStyle.PRIMARY) 102 | async def modal_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: 103 | modal = ConfessionModal(title="Submit Confession/Question") 104 | # await ctx.respond_with_modal(modal) 105 | await modal.send(ctx.interaction) 106 | 107 | class AppealModalView(miru.View): 108 | @miru.button(label="Click to Submit", style=hikari.ButtonStyle.PRIMARY) 109 | async def modal_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: 110 | modal = AppealModal(title="Submit Ban Appeal") 111 | # await ctx.respond_with_modal(modal) 112 | await modal.send(ctx.interaction) 113 | 114 | def createConfessionEmbed(confessionId, confession, title): 115 | color = random.randrange(0, 2**24) 116 | color = hex(color) 117 | embed = ( 118 | hikari.Embed( 119 | title = str(confessionId) + ": " + title , 120 | color = color 121 | ) 122 | ).add_field(name="Confession/Question", value=confession) 123 | return embed -------------------------------------------------------------------------------- /utils/NoDriverBrowserCreator.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | import nodriver as uc 4 | import globals 5 | import asyncio 6 | import ctypes, os 7 | import platform 8 | import psutil 9 | import logging 10 | try: 11 | from AppConstants import Constants as Constants 12 | except ImportError: 13 | from DefaultConstants import Constants as Constants 14 | from nodriver import * 15 | 16 | logger = logging.getLogger(__name__) 17 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 18 | 19 | def KillUnconncetedBrowsers(): 20 | PROCNAMES = ["google-chrome", 21 | "chromium", 22 | "chromium-browser", 23 | "chrome", 24 | "google-chrome-stable"] 25 | numBrowserProcesses = 0 26 | for proc in psutil.process_iter(): 27 | # check whether the process name matches 28 | if proc.name() in PROCNAMES: 29 | numBrowserProcesses = numBrowserProcesses + 1 30 | try: 31 | proc.terminate() 32 | except Exception as e: 33 | proc.kill() 34 | logger.warning(e) 35 | if numBrowserProcesses > 0: 36 | logger.info(f"Tried to kill {numBrowserProcesses} browser processes.") 37 | 38 | def getUserAgent(): 39 | userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/127.0.2651.105" 40 | try: 41 | page = requests.get('https://jnrbsn.github.io/user-agents/user-agents.json') 42 | userAgentsJson = page.json() 43 | userAgent = random.choice(userAgentsJson) 44 | page.close() 45 | except: 46 | logger.warning("Trouble getting user agent from jnrbsn's github. Using default") 47 | return userAgent 48 | 49 | async def GetBrowser(proxy=""): 50 | while globals.browserOpen: 51 | await asyncio.sleep(2 * Constants.NODRIVER_WAIT_MULTIPLIER) 52 | try: 53 | globals.browserOpen = True 54 | await asyncio.sleep(1 * Constants.NODRIVER_WAIT_MULTIPLIER) 55 | toSandbox = not IsRoot() 56 | toHeadless = False if platform.system() == "Linux" else True 57 | dataDir = "/ndTemp" if platform.system() == "Linux" else None 58 | if proxy: 59 | browser = await uc.start(sandbox=toSandbox, 60 | headless=toHeadless, 61 | browser_args=[f'--proxy-server={proxy}','--mute-audio','--disable-3d-apis','--disable-dev-shm-usage','--disable-gpu','--disable-blink-features=AutomationControlled'], 62 | retries = Constants.NODRIVER_BROWSER_CONNECT_RETRIES, 63 | user_data_dir=dataDir 64 | ) 65 | else: 66 | browser = await uc.start(sandbox=toSandbox, 67 | headless=toHeadless, 68 | retries = Constants.NODRIVER_BROWSER_CONNECT_RETRIES, 69 | user_data_dir=dataDir 70 | ) 71 | except Exception as e: 72 | logger.warning(f"error creating browser in GetBrowser: {e}") 73 | await asyncio.sleep(1 * Constants.NODRIVER_WAIT_MULTIPLIER) 74 | KillUnconncetedBrowsers() 75 | await asyncio.sleep(1 * Constants.NODRIVER_WAIT_MULTIPLIER) 76 | globals.browserOpen = False 77 | return browser 78 | 79 | # Taken from https://github.com/ultrafunkamsterdam/nodriver/blob/1bb6003c7f0db4d3ec05fdf3fc8c8e0804260103/nodriver/core/config.py#L240 80 | def IsRoot(): 81 | """ 82 | helper function to determine if user trying to launch chrome 83 | under linux as root, which needs some alternative handling 84 | :return: 85 | :rtype: 86 | """ 87 | 88 | try: 89 | return os.getuid() == 0 90 | except AttributeError: 91 | return ctypes.windll.shell32.IsUserAnAdmin() != 0 -------------------------------------------------------------------------------- /utils/Notifications.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from utils.EmbedCreator import EmbedCreator 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | from utils.Database import Database 8 | import time 9 | import hikari 10 | 11 | class Notifications: 12 | async def OFNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, ofUserName, isRerun): 13 | OfLiveStreamUrl = f"https://onlyfans.com/{ofUserName}/live" 14 | ofOnlineText = Constants.ofAboveEmbedText +"\n<" + OfLiveStreamUrl + ">" 15 | embedMaker = EmbedCreator( 16 | Constants.ofBelowTitleText, 17 | title, 18 | OfLiveStreamUrl, 19 | 'images/platformImages/OFImage.png', 20 | Constants.ofEmbedColor, 21 | icon, 22 | ofUserName 23 | ) 24 | task = asyncio.create_task(embedMaker.getEmbed()) 25 | ofEmbed = await task 26 | db = Database() 27 | db.updatePlatformRowCol("onlyfans","last_online_message",time.time()) 28 | db.updatePlatformAccountRowCol("onlyfans",ofUserName,"last_online_message",time.time()) 29 | IS_PING = db.getPing() 30 | rolesToPing = Constants.OF_RERUN_ROLES_TO_PING if isRerun else Constants.OF_ROLES_TO_PING 31 | messageContent = rolesToPing + ofOnlineText if IS_PING else ofOnlineText 32 | await rest.create_message(channel = Constants.OF_NOTIFICATION_CHANNEL_ID, content = messageContent, embed = ofEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 33 | 34 | async def ChaturNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, cbUserName, isRerun): 35 | cbLiveStreamUrl = f"https://chaturbate.com/{cbUserName}/" 36 | cbOnlineText = Constants.cbAboveEmbedText + "\n<" + cbLiveStreamUrl + ">" 37 | embedMaker = EmbedCreator( 38 | Constants.cbBelowTitleText, 39 | title, 40 | cbLiveStreamUrl, 41 | 'images/platformImages/CbImage.png', 42 | Constants.cbEmbedColor, 43 | icon, 44 | cbUserName, 45 | largeThumbnail= largeThumbnail 46 | ) 47 | task = asyncio.create_task(embedMaker.getEmbed()) 48 | cbEmbed = await task 49 | db = Database() 50 | db.updatePlatformRowCol("chaturbate","last_online_message",time.time()) 51 | db.updatePlatformAccountRowCol("chaturbate",cbUserName,"last_online_message",time.time()) 52 | IS_PING = db.getPing() 53 | rolesToPing = Constants.CB_RERUN_ROLES_TO_PING if isRerun else Constants.CB_ROLES_TO_PING 54 | messageContent = rolesToPing + cbOnlineText if IS_PING else cbOnlineText 55 | await rest.create_message(channel = Constants.CB_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=cbEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 56 | 57 | async def FansNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, fansUserName, isRerun): 58 | fansLiveStreamUrl = f"https://fansly.com/live/{fansUserName}" 59 | fansOnlineText = Constants.fansAboveEmbedText + "\n<" + fansLiveStreamUrl + ">" 60 | embedMaker = EmbedCreator( 61 | Constants.fansBelowTitleText, 62 | title, 63 | fansLiveStreamUrl, 64 | 'images/platformImages/FansImage.png', 65 | Constants.fansEmbedColor, 66 | icon, 67 | fansUserName 68 | ) 69 | task = asyncio.create_task(embedMaker.getEmbed()) 70 | fansEmbed = await task 71 | db = Database() 72 | db.updatePlatformRowCol("fansly","last_online_message",time.time()) 73 | db.updatePlatformAccountRowCol("fansly",fansUserName,"last_online_message",time.time()) 74 | IS_PING = db.getPing() 75 | rolesToPing = Constants.FANS_RERUN_ROLES_TO_PING if isRerun else Constants.FANS_ROLES_TO_PING 76 | messageContent = rolesToPing + fansOnlineText if IS_PING else fansOnlineText 77 | await rest.create_message(channel = Constants.FANS_NOTIFICATION_CHANNEL_ID, content = messageContent, embed = fansEmbed, mentions_everyone=IS_PING, role_mentions=IS_PING) 78 | 79 | async def TwitchNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, twitchUserName, isRerun): 80 | twitchLiveStreamUrl = f"https://www.twitch.tv/{twitchUserName}" 81 | twitchOnlineText = Constants.twitchAboveEmbedText + "\n<" + twitchLiveStreamUrl + ">" 82 | embedMaker = EmbedCreator( 83 | Constants.twitchBelowTitleText, 84 | title, 85 | twitchLiveStreamUrl, 86 | 'images/platformImages/twitchImage.png', 87 | Constants.twitchEmbedColor, 88 | icon, 89 | twitchUserName, 90 | largeThumbnail= largeThumbnail 91 | ) 92 | task = asyncio.create_task(embedMaker.getEmbed()) 93 | twitchEmbed = await task 94 | db = Database() 95 | db.updatePlatformRowCol("twitch","last_online_message",time.time()) 96 | db.updatePlatformAccountRowCol("twitch",twitchUserName,"last_online_message",time.time()) 97 | IS_PING = db.getPing() 98 | rolesToPing = Constants.TWITCH_RERUN_ROLES_TO_PING if isRerun else Constants.TWITCH_ROLES_TO_PING 99 | messageContent = rolesToPing + twitchOnlineText if IS_PING else twitchOnlineText 100 | await rest.create_message(channel = Constants.TWITCH_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=twitchEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 101 | 102 | async def YTNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, ytUserName, isRerun): 103 | ytLiveStreamUrl = f"https://www.youtube.com/@{ytUserName}/live" 104 | ytOnlineText = Constants.ytAboveEmbedText + "\n<" + ytLiveStreamUrl + ">" 105 | embedMaker = EmbedCreator( 106 | Constants.ytBelowTitleText, 107 | title, 108 | ytLiveStreamUrl, 109 | 'images/platformImages/ytImage.png', 110 | Constants.ytEmbedColor, 111 | icon, 112 | ytUserName, 113 | largeThumbnail= largeThumbnail 114 | ) 115 | task = asyncio.create_task(embedMaker.getEmbed()) 116 | ytEmbed = await task 117 | db = Database() 118 | db.updatePlatformRowCol("youtube","last_online_message",time.time()) 119 | db.updatePlatformAccountRowCol("youtube",ytUserName,"last_online_message",time.time()) 120 | IS_PING = db.getPing() 121 | rolesToPing = Constants.YT_RERUN_ROLES_TO_PING if isRerun else Constants.YT_ROLES_TO_PING 122 | messageContent = rolesToPing + ytOnlineText if IS_PING else ytOnlineText 123 | await rest.create_message(channel = Constants.YT_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=ytEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 124 | 125 | async def KickNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, kickUserName, isRerun): 126 | kickLiveStreamUrl = f"https://kick.com/{kickUserName}" 127 | kickOnlineText = Constants.kickAboveEmbedText + "\n<" + kickLiveStreamUrl + ">" 128 | embedMaker = EmbedCreator( 129 | Constants.kickBelowTitleText, 130 | title, 131 | kickLiveStreamUrl, 132 | 'images/platformImages/KickImage.png', 133 | Constants.kickEmbedColor, 134 | icon, 135 | kickUserName, 136 | largeThumbnail= largeThumbnail 137 | ) 138 | task = asyncio.create_task(embedMaker.getEmbed()) 139 | kickEmbed = await task 140 | db = Database() 141 | db.updatePlatformRowCol("kick","last_online_message",time.time()) 142 | db.updatePlatformAccountRowCol("kick",kickUserName,"last_online_message",time.time()) 143 | IS_PING = db.getPing() 144 | rolesToPing = Constants.KICK_RERUN_ROLES_TO_PING if isRerun else Constants.KICK_ROLES_TO_PING 145 | messageContent = rolesToPing + kickOnlineText if IS_PING else kickOnlineText 146 | await rest.create_message(channel = Constants.KICK_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=kickEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 147 | 148 | async def Cam4Notification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, cam4UserName, isRerun): 149 | cam4LiveStreamUrl = f"https://www.cam4.com/{cam4UserName}" 150 | cam4OnlineText = Constants.cam4AboveEmbedText + "\n<" + cam4LiveStreamUrl + ">" 151 | embedMaker = EmbedCreator( 152 | Constants.cam4BelowTitleText, 153 | title, 154 | cam4LiveStreamUrl, 155 | 'images/platformImages/cam4Image.png', 156 | Constants.cam4EmbedColor, 157 | icon, 158 | cam4UserName, 159 | largeThumbnail= largeThumbnail 160 | ) 161 | task = asyncio.create_task(embedMaker.getEmbed()) 162 | cam4Embed = await task 163 | db = Database() 164 | db.updatePlatformRowCol("cam4","last_online_message",time.time()) 165 | db.updatePlatformAccountRowCol("cam4",cam4UserName,"last_online_message",time.time()) 166 | IS_PING = db.getPing() 167 | rolesToPing = Constants.CAM4_RERUN_ROLES_TO_PING if isRerun else Constants.CAM4_ROLES_TO_PING 168 | messageContent = rolesToPing + cam4OnlineText if IS_PING else cam4OnlineText 169 | await rest.create_message(channel = Constants.CAM4_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=cam4Embed, mentions_everyone= IS_PING, role_mentions=IS_PING) 170 | 171 | async def MfcNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, mfcUserName, isRerun): 172 | mfcLiveStreamUrl = f"https://www.myfreecams.com/#{mfcUserName}" 173 | mfcOnlineText = Constants.mfcAboveEmbedText + "\n<" + mfcLiveStreamUrl + ">" 174 | embedMaker = EmbedCreator( 175 | Constants.mfcBelowTitleText, 176 | title, 177 | mfcLiveStreamUrl, 178 | 'images/platformImages/mfcImage.png', 179 | Constants.mfcEmbedColor, 180 | icon, 181 | mfcUserName, 182 | largeThumbnail= largeThumbnail 183 | ) 184 | task = asyncio.create_task(embedMaker.getEmbed()) 185 | mfcEmbed = await task 186 | db = Database() 187 | db.updatePlatformRowCol("mfc","last_online_message",time.time()) 188 | db.updatePlatformAccountRowCol("mfc",mfcUserName,"last_online_message",time.time()) 189 | IS_PING = db.getPing() 190 | rolesToPing = Constants.MFC_RERUN_ROLES_TO_PING if isRerun else Constants.MFC_ROLES_TO_PING 191 | messageContent = rolesToPing + mfcOnlineText if IS_PING else mfcOnlineText 192 | await rest.create_message(channel = Constants.MFC_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=mfcEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 193 | 194 | async def BcNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, bcUserName, isRerun): 195 | bcLiveStreamUrl = f"https://bongacams.com/{bcUserName}" 196 | bcOnlineText = Constants.bcAboveEmbedText + "\n<" + bcLiveStreamUrl + ">" 197 | embedMaker = EmbedCreator( 198 | Constants.bcBelowTitleText, 199 | title, 200 | bcLiveStreamUrl, 201 | 'images/platformImages/bcImage.png', 202 | Constants.bcEmbedColor, 203 | icon, 204 | bcUserName, 205 | largeThumbnail= largeThumbnail 206 | ) 207 | task = asyncio.create_task(embedMaker.getEmbed()) 208 | bcEmbed = await task 209 | db = Database() 210 | db.updatePlatformRowCol("bongacams","last_online_message",time.time()) 211 | db.updatePlatformAccountRowCol("bongacams",bcUserName,"last_online_message",time.time()) 212 | IS_PING = db.getPing() 213 | rolesToPing = Constants.BC_RERUN_ROLES_TO_PING if isRerun else Constants.BC_ROLES_TO_PING 214 | messageContent = rolesToPing + bcOnlineText if IS_PING else bcOnlineText 215 | await rest.create_message(channel = Constants.BC_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=bcEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 216 | 217 | async def ScNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, scUserName, isRerun): 218 | scLiveStreamUrl = f"https://stripchat.com/{scUserName}" 219 | scOnlineText = Constants.scAboveEmbedText + "\n<" + scLiveStreamUrl + ">" 220 | embedMaker = EmbedCreator( 221 | Constants.scBelowTitleText, 222 | title, 223 | scLiveStreamUrl, 224 | 'images/platformImages/scImage.png', 225 | Constants.scEmbedColor, 226 | icon, 227 | scUserName, 228 | largeThumbnail= largeThumbnail 229 | ) 230 | task = asyncio.create_task(embedMaker.getEmbed()) 231 | scEmbed = await task 232 | db = Database() 233 | db.updatePlatformRowCol("stripchat","last_online_message",time.time()) 234 | db.updatePlatformAccountRowCol("stripchat",scUserName,"last_online_message",time.time()) 235 | IS_PING = db.getPing() 236 | rolesToPing = Constants.SC_RERUN_ROLES_TO_PING if isRerun else Constants.SC_ROLES_TO_PING 237 | messageContent = rolesToPing + scOnlineText if IS_PING else scOnlineText 238 | await rest.create_message(channel = Constants.SC_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=scEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 239 | 240 | async def EpNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, epUserName, isRerun): 241 | epLiveStreamUrl = f"https://eplay.com/{epUserName}/live" 242 | epOnlineText = Constants.epAboveEmbedText + "\n<" + epLiveStreamUrl + ">" 243 | embedMaker = EmbedCreator( 244 | Constants.epBelowTitleText, 245 | title, 246 | epLiveStreamUrl, 247 | 'images/platformImages/epImage.png', 248 | Constants.epEmbedColor, 249 | icon, 250 | epUserName, 251 | largeThumbnail= largeThumbnail 252 | ) 253 | task = asyncio.create_task(embedMaker.getEmbed()) 254 | epEmbed = await task 255 | db = Database() 256 | db.updatePlatformRowCol("eplay","last_online_message",time.time()) 257 | db.updatePlatformAccountRowCol("eplay",epUserName,"last_online_message",time.time()) 258 | IS_PING = db.getPing() 259 | rolesToPing = Constants.EP_RERUN_ROLES_TO_PING if isRerun else Constants.EP_ROLES_TO_PING 260 | messageContent = rolesToPing + epOnlineText if IS_PING else epOnlineText 261 | await rest.create_message(channel = Constants.EP_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=epEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) 262 | 263 | async def MvNotification(rest: hikari.impl.RESTClientImpl, title, largeThumbnail, icon, mvUserName, isRerun): 264 | mvLiveStreamUrl = f"https://www.manyvids.com/live/cam/{mvUserName.lower()}" 265 | mvOnlineText = Constants.mvAboveEmbedText + "\n<" + mvLiveStreamUrl + ">" 266 | embedMaker = EmbedCreator( 267 | Constants.mvBelowTitleText, 268 | title, 269 | mvLiveStreamUrl, 270 | 'images/platformImages/mvImage.png', 271 | Constants.mvEmbedColor, 272 | icon, 273 | mvUserName, 274 | largeThumbnail= largeThumbnail 275 | ) 276 | task = asyncio.create_task(embedMaker.getEmbed()) 277 | mvEmbed = await task 278 | db = Database() 279 | db.updatePlatformRowCol("manyvids","last_online_message",time.time()) 280 | db.updatePlatformAccountRowCol("manyvids",mvUserName,"last_online_message",time.time()) 281 | IS_PING = db.getPing() 282 | rolesToPing = Constants.MV_RERUN_ROLES_TO_PING if isRerun else Constants.MV_ROLES_TO_PING 283 | messageContent = rolesToPing + mvOnlineText if IS_PING else mvOnlineText 284 | await rest.create_message(channel = Constants.MV_NOTIFICATION_CHANNEL_ID, content = messageContent, embed=mvEmbed, mentions_everyone= IS_PING, role_mentions=IS_PING) -------------------------------------------------------------------------------- /utils/StaticMethods.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | from utils.Database import Database 4 | import globals 5 | import datetime 6 | try: 7 | from AppConstants import Constants as Constants 8 | except ImportError: 9 | from DefaultConstants import Constants as Constants 10 | from datetime import timedelta 11 | from datetime import date 12 | import re 13 | import tanjun 14 | import logging 15 | 16 | logger = logging.getLogger(__name__) 17 | logger.setLevel(Constants.SASSBOT_LOG_LEVEL) 18 | 19 | def logCommand(funcName, ctx) -> None: 20 | file = open("commandLogs.txt", 'a') 21 | date = datetime.datetime.fromtimestamp(time.time()) 22 | if isinstance(ctx, tanjun.abc.SlashContext): 23 | file.write(f"{date} - {funcName} - used by {ctx.member.id} aka {ctx.member.display_name}\n") 24 | else: 25 | logger.warning("didn't get right ctx for command logger") 26 | file.close() 27 | 28 | def resetUnfinishedConfessions(): 29 | db = Database() 30 | unFinished = db.getUnfinishedConfessionReviews() 31 | if unFinished: 32 | for row in unFinished: 33 | if timeToSeconds(row[1]) >= Constants.TIME_BEFORE_REVIEW_RESET: 34 | db.resetConfessionDateReviewed(row[0]) 35 | 36 | def resetUnfinishedAppeals(): 37 | db = Database() 38 | unFinished = db.getUnfinishedAppealReviews() 39 | if unFinished: 40 | for row in unFinished: 41 | if timeToSeconds(row[1]) >= Constants.TIME_BEFORE_REVIEW_RESET: 42 | db.resetAppealDateReviewed(row[0]) 43 | 44 | async def isPermission(ctx: tanjun.abc.SlashContext)-> bool: 45 | hasPermission = False 46 | roles = ctx.member.get_roles() 47 | for role in roles: 48 | if role.id in Constants.whiteListedRoleIDs or ctx.member.id in Constants.whiteListedRoleIDs: 49 | hasPermission = True 50 | if not hasPermission: 51 | await ctx.respond("You don't have permission to do this",) 52 | return hasPermission 53 | 54 | def isRerun(title:str) -> bool: 55 | reString = "(?i).*([^a-zA-Z]|^)+((rerun|rr|𝓡𝓡|𝓡𝓔𝓡𝓤𝓝|not live))([^a-zA-Z]|$)+.*" 56 | rerun = re.search(reString,title) 57 | return rerun 58 | 59 | def replaceIntsWithString(minsDict: dict) -> dict: 60 | popKeys = [] 61 | minsDict = minsDict.copy() 62 | for k, v in minsDict.items(): 63 | if minsDict[k] == 0: 64 | popKeys.append(k) 65 | else: 66 | minsDict[k] = minutesToHourMinString(minsDict[k]) 67 | for key in popKeys: 68 | minsDict.pop(key) 69 | return minsDict 70 | 71 | def minutesToHourMinString(minutes: int) -> str: 72 | hours = int(minutes / 60) 73 | mins = int(minutes % 60) 74 | return f"{hours}:{mins}" 75 | 76 | def getWeekStreamingMinutes(startingDate: date, minutesDict = {}): 77 | db = Database() 78 | minutesBetweenOnlineChecks = 10 79 | daysInWeek = 7 80 | weekMinutes = {"CB":0,"OF":0,"Twitch":0,"YT":0,"Fans":0,"Kick":0,"Cam4":0, "MFC":0, "BC":0 , "SC":0,"EP":0,"MV":0,"TotalTimeStreamingLive":0, "TotalTimeStreamingReruns":0} if not minutesDict else dict(minutesDict) 81 | platforms = list(weekMinutes) 82 | platforms.remove("TotalTimeStreamingLive") 83 | platforms.remove("TotalTimeStreamingReruns") 84 | for i in range(daysInWeek): 85 | pastDate = startingDate - timedelta(days = i) 86 | dayData = db.getPresenceDay(pastDate) 87 | if dayData: 88 | for k, v in dayData.items(): 89 | if v: 90 | if 'streaming' in v.keys(): 91 | notOnlyRerunsFlag = False 92 | if isinstance(v['streaming'],str): 93 | if "RR" in v['streaming']: 94 | weekMinutes["TotalTimeStreamingReruns"] += minutesBetweenOnlineChecks 95 | for platform in platforms: 96 | if platform in v['streaming'] and not "RR-" + platform in v['streaming']: 97 | weekMinutes[platform] += minutesBetweenOnlineChecks 98 | notOnlyRerunsFlag = True 99 | if notOnlyRerunsFlag: 100 | weekMinutes["TotalTimeStreamingLive"] += minutesBetweenOnlineChecks 101 | return weekMinutes 102 | 103 | def smartRebroadcast() -> None: 104 | platforms = ['chaturbate','onlyfans','fansly','twitch','youtube','kick','cam4','mfc','bongacams', 'stripchat','eplay','manyvids'] 105 | db = Database() 106 | for platform in platforms: 107 | lastOnlineMessage,streamStartTime,streamEndTime,isRerun = db.getPlatformsRowValues(platform) 108 | secondsSinceLastMessage = timeToSeconds(lastOnlineMessage) 109 | if secondsSinceLastMessage >= Constants.SECONDS_BETWEEN_SMART_ALERTS and streamStartTime > streamEndTime: 110 | logger.info(f"Smart alert for {platform}") 111 | globals.rebroadcast[platform] = 1 112 | 113 | def getMaxOnlineInPresenceDict(presDict: dict) -> int: 114 | maxOnline = 0 115 | for k, v in presDict.items(): 116 | if v: 117 | maxOnline = max(v["online"], maxOnline) 118 | return maxOnline 119 | 120 | 121 | def getHourMinuteString(offset = 0): 122 | hour = datetime.datetime.now().hour 123 | hour += offset 124 | hour %= 24 125 | minute = datetime.datetime.now().minute 126 | minute = minute - (minute%10) 127 | hourMinute = f"{hour}:{minute}" 128 | return hourMinute 129 | 130 | def setOfflineAddTime(): 131 | db = Database() 132 | db.setStreamLastOffline(time.time()) 133 | lastOnline,lastOffline,lastTotalStreamLength = db.getStreamTableValues() 134 | newTotalStreamLength = lastTotalStreamLength + (lastOffline - lastOnline) 135 | db.setStreamLastStreamLength(newTotalStreamLength) 136 | 137 | def getEmbedImage() -> str: 138 | db = Database() 139 | twImgList, twImgQue, bannedList = db.getTwImgStuff() 140 | url = checkImagePin() 141 | if not twImgQue and twImgList: 142 | twImgQue = twImgList 143 | if not twImgList: 144 | imageSrc = Constants.defaultThumbnail 145 | logger.info("adding default image for embed since nothing is on the image list.") 146 | elif url: 147 | imageSrc = url 148 | else: 149 | imageSrc = twImgQue.pop(0) 150 | db.setTwImgQueue(twImgQue) 151 | return imageSrc 152 | 153 | def unPin() -> None: 154 | db = Database() 155 | db.setImgPin(0, "") 156 | 157 | def setRebroadcast() -> None: 158 | logger.info("rebroadcast: On") 159 | globals.rebroadcast = { 160 | "chaturbate":1, 161 | "onlyfans":1, 162 | "fansly":1, 163 | "twitch":1, 164 | "youtube":1, 165 | "kick":1, 166 | "cam4":1, 167 | "mfc":1, 168 | "bongacams":1, 169 | "stripchat":1, 170 | "eplay":1, 171 | "manyvids":1 172 | } 173 | 174 | def addImageListQue(url: str) -> None: 175 | db = Database() 176 | twImgList, twImgQue,bannedList = db.getTwImgStuff() 177 | twImgList.insert(0, url) 178 | db.setTwImgList(twImgList) 179 | twImgQue.insert(0,url) 180 | db.setTwImgQueue(twImgQue) 181 | 182 | def pinImage(url: str, hours: int) -> None: 183 | currentTime = time.time() 184 | addedSeconds = hours * 60 * 60 185 | pinEndEpochTime = currentTime + addedSeconds 186 | db = Database() 187 | db.setImgPin(pinEndEpochTime, url) 188 | 189 | def checkImagePin(): 190 | url = "" 191 | db = Database() 192 | pinTime, pinUrl = db.getImgPin() 193 | if pinTime is None: 194 | pinTime = 0 195 | if time.time() < pinTime: 196 | url = pinUrl 197 | return url 198 | 199 | def checkOnline(db: Database) -> str: 200 | playingString = "" 201 | cbLastOnlineMessage,cbStreamStartTime,cbStreamEndTime, cbIsRerun = db.getPlatformsRowValues('chaturbate') 202 | ofLastOnlineMessage,ofStreamStartTime,ofStreamEndTime, ofIsRerun = db.getPlatformsRowValues('onlyfans') 203 | twitchLastOnlineMessage,twitchStreamStartTime,twitchStreamEndTime, twitchIsRerun = db.getPlatformsRowValues('twitch') 204 | ytLastOnlineMessage,ytStreamStartTime,ytStreamEndTime, ytIsRerun = db.getPlatformsRowValues('youtube') 205 | fansLastOnlineMessage,fansStreamStartTime,fansStreamEndTime, fansIsRerun = db.getPlatformsRowValues('fansly') 206 | kickLastOnlineMessage,kickStreamStartTime,kickStreamEndTime, kickIsRerun = db.getPlatformsRowValues('kick') 207 | cam4LastOnlineMessage,cam4StreamStartTime,cam4StreamEndTime, cam4IsRerun = db.getPlatformsRowValues('cam4') 208 | mfcLastOnlineMessage,mfcStreamStartTime,mfcStreamEndTime, mfcIsRerun = db.getPlatformsRowValues('mfc') 209 | bcLastOnlineMessage,bcStreamStartTime,bcStreamEndTime, bcIsRerun = db.getPlatformsRowValues('bongacams') 210 | scLastOnlineMessage,scStreamStartTime,scStreamEndTime, scIsRerun = db.getPlatformsRowValues('stripchat') 211 | epLastOnlineMessage,epStreamStartTime,epStreamEndTime, epIsRerun = db.getPlatformsRowValues('eplay') 212 | mvLastOnlineMessage,mvStreamStartTime,mvStreamEndTime, mvIsRerun = db.getPlatformsRowValues('manyvids') 213 | if cbStreamStartTime > cbStreamEndTime: 214 | playingString = playingString + "RR-CB " if cbIsRerun else playingString + "CB " 215 | if ofStreamStartTime > ofStreamEndTime: 216 | playingString = playingString + "RR-OF " if ofIsRerun else playingString + "OF " 217 | if twitchStreamStartTime > twitchStreamEndTime: 218 | playingString = playingString + "RR-Twitch " if twitchIsRerun else playingString + "Twitch " 219 | if ytStreamStartTime > ytStreamEndTime: 220 | playingString = playingString + "RR-YT " if ytIsRerun else playingString + "YT " 221 | if fansStreamStartTime > fansStreamEndTime: 222 | playingString = playingString + "RR-Fans " if fansIsRerun else playingString + "Fans " 223 | if kickStreamStartTime > kickStreamEndTime: 224 | playingString = playingString + "RR-Kick " if kickIsRerun else playingString + "Kick " 225 | if cam4StreamStartTime > cam4StreamEndTime: 226 | playingString = playingString + "RR-Cam4 " if cam4IsRerun else playingString + "Cam4 " 227 | if mfcStreamStartTime > mfcStreamEndTime: 228 | playingString = playingString + "RR-MFC " if mfcIsRerun else playingString + "MFC " 229 | if bcStreamStartTime > bcStreamEndTime: 230 | playingString = playingString + "RR-BC " if bcIsRerun else playingString + "BC " 231 | if scStreamStartTime > scStreamEndTime: 232 | playingString = playingString + "RR-SC " if scIsRerun else playingString + "SC " 233 | if epStreamStartTime > epStreamEndTime: 234 | playingString = playingString + "RR-EP " if epIsRerun else playingString + "EP " 235 | if mvStreamStartTime > mvStreamEndTime: 236 | playingString = playingString + "RR-MV " if mvIsRerun else playingString + "MV " 237 | if playingString and playingString[-1] == " ": 238 | playingString = playingString[:-1] 239 | return playingString 240 | 241 | def timeToHoursMinutes(newTime: float) -> int: 242 | totalTime = time.time() - newTime 243 | totalTime = int(totalTime) 244 | totalTimeSeconds = int(totalTime % 60) 245 | totalTimeMinutes = int((totalTime - totalTimeSeconds) / 60) 246 | leftoverMinutes = totalTimeMinutes % 60 247 | totalTimeHours = int((totalTimeMinutes - leftoverMinutes ) / 60) 248 | return totalTimeHours, leftoverMinutes 249 | 250 | def timeToHoursMinutesStartEnd(startTime: float, endTime: float) -> int: 251 | totalTime = endTime - startTime 252 | totalTime = int(totalTime) 253 | totalTimeSeconds = int(totalTime % 60) 254 | totalTimeMinutes = int((totalTime - totalTimeSeconds) / 60) 255 | leftoverMinutes = totalTimeMinutes % 60 256 | totalTimeHours = int((totalTimeMinutes - leftoverMinutes ) / 60) 257 | return totalTimeHours, leftoverMinutes 258 | 259 | def timeToHoursMinutesTotalTime(totalTime: float) -> int: 260 | totalTime = int(totalTime) 261 | totalTimeSeconds = int(totalTime % 60) 262 | totalTimeMinutes = int((totalTime - totalTimeSeconds) / 60) 263 | leftoverMinutes = totalTimeMinutes % 60 264 | totalTimeHours = int((totalTimeMinutes - leftoverMinutes ) / 60) 265 | return totalTimeHours, leftoverMinutes 266 | 267 | def timeToSeconds(newTime: float) -> int: 268 | if newTime == None: 269 | newTime = 0 270 | totalTime = time.time() - newTime 271 | totalTime = int(totalTime) 272 | return totalTime 273 | 274 | def rebootServer() -> None: 275 | logger.critical("Sassbot server rebooting from restart command or fd leak detection or scheduled restart based off TIME_BEFORE_BOT_RESTART") 276 | globals.botStartTime = time.time() 277 | os.system('reboot') 278 | 279 | def safeRebootServer() -> None: 280 | logger.warning("Scheduled restart is happening.\nSleeping for 300 seconds before restart, in case something goes horribly wrong") 281 | time.sleep(300) 282 | rebootServer() 283 | 284 | def GetThumbnail(tempThumbUrl, constantsThumbnail): 285 | thumbnail = "" 286 | if constantsThumbnail == "LIST": 287 | thumbnail = "" 288 | elif constantsThumbnail: 289 | thumbnail = constantsThumbnail 290 | elif tempThumbUrl: 291 | thumbnail = tempThumbUrl 292 | 293 | return thumbnail 294 | 295 | def GetProxies(proxyIpPort): 296 | proxies = { 297 | 'http': f'socks5://{proxyIpPort}', 298 | 'https': f'socks5://{proxyIpPort}' 299 | } 300 | return proxies 301 | -------------------------------------------------------------------------------- /utils/bot.py: -------------------------------------------------------------------------------- 1 | import hikari 2 | import tanjun 3 | try: 4 | from AppConstants import Constants as Constants 5 | except ImportError: 6 | from DefaultConstants import Constants as Constants 7 | import miru 8 | 9 | def getToken(): 10 | with open("./secrets/token") as file: 11 | token = file.read().strip() 12 | return str(token) 13 | 14 | def build_bot() -> hikari.GatewayBot: 15 | TOKEN = getToken() 16 | bot = hikari.GatewayBot(TOKEN, 17 | intents=hikari.Intents.ALL_UNPRIVILEGED | hikari.Intents.MESSAGE_CONTENT | hikari.Intents.GUILD_MEMBERS | hikari.Intents.GUILD_PRESENCES 18 | ) 19 | make_client(bot) 20 | miru.install(bot) 21 | return bot 22 | 23 | def make_client(bot: hikari.GatewayBot) -> tanjun.Client: 24 | client = ( 25 | tanjun.Client.from_gateway_bot( 26 | bot, 27 | mention_prefix=True, 28 | declare_global_commands=Constants.GUILD_ID 29 | ) 30 | ).add_prefix("!") 31 | client.load_modules("plugins.checks") 32 | client.load_modules("plugins.commands") 33 | client.load_modules("plugins.listeners") 34 | return client --------------------------------------------------------------------------------