You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

292 lines
12KB

  1. # -*- coding: utf-8 -*-
  2. # MIT License
  3. #
  4. # Copyright (c) 2017-2018 Roxanne Gibson
  5. #
  6. # Permission is hereby granted, free of charge, to any person obtaining a copy
  7. # of this software and associated documentation files (the "Software"), to deal
  8. # in the Software without restriction, including without limitation the rights
  9. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. # copies of the Software, and to permit persons to whom the Software is
  11. # furnished to do so, subject to the following conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included in all
  14. # copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. # SOFTWARE.
  23. import fnmatch
  24. import random
  25. from html import unescape
  26. import discord
  27. from bs4 import BeautifulSoup
  28. from discord.ext import commands
  29. import roxbot
  30. class Scrapper:
  31. """Scrapper is a class to aid in the scrapping of reddit subreddit's of images. (includes small amount of video support)
  32. This includes its own caching system."""
  33. # TODO: Reimplement eroshare, eroshae, and erome support.
  34. # Also implement better api interaction with imgur now we require api key
  35. def __init__(self, cache_limit=10):
  36. self.post_cache = {}
  37. self.cache_limit = cache_limit
  38. @staticmethod
  39. async def _imgur_removed(url):
  40. try:
  41. page = await roxbot.http.get_page(url)
  42. except UnicodeDecodeError:
  43. return False # This is if it is an image with a weird url
  44. soup = BeautifulSoup(page, 'html.parser')
  45. if "404 Not Found" in soup.title.string:
  46. return True
  47. try:
  48. return bool("removed.png" in soup.img["src"])
  49. except TypeError: # This should protect roxbot in case bs4 returns nothing.
  50. return False
  51. async def imgur_get(self, url):
  52. extensions = ("png", "jpg", "jpeg", "gif", "gifv", "mp4", "webm", "webp")
  53. for ext in extensions:
  54. if fnmatch.fnmatch(url.split(".")[-1], ext+"*"):
  55. # This is fixing issues where urls will have ".png?2" or some shit
  56. return url
  57. if await self._imgur_removed(url):
  58. return False
  59. if not roxbot.imgur_token:
  60. return False
  61. base_endpoint = "https://api.imgur.com/3/"
  62. endpoint_album = base_endpoint + "album/{}/images.json".format(url.split("/")[-1])
  63. endpoint_image = base_endpoint + "image/{}.json".format(url.split("/")[-1])
  64. try:
  65. resp = await roxbot.http.api_request(endpoint_image, headers={"Authorization": "Client-ID {}".format(roxbot.imgur_token)})
  66. if bool(resp["success"]) is True:
  67. return resp["data"]["link"]
  68. else:
  69. resp = await roxbot.http.api_request(endpoint_album, headers={"Authorization": "Client-ID {}".format(roxbot.imgur_token)})
  70. return resp["data"][0]["link"]
  71. except TypeError:
  72. return False
  73. async def parse_url(self, url):
  74. if url.split(".")[-1] in ("png", "jpg", "jpeg", "gif", "gifv", "webm", "mp4", "webp"):
  75. return url
  76. if "imgur" in url:
  77. return await self.imgur_get(url)
  78. elif "eroshare" in url or "eroshae" in url or "erome" in url:
  79. return None
  80. # return ero_get(url)
  81. elif "gfycat" in url or "redd.it" in url or "i.reddituploads" in url or "media.tumblr" in url or "streamable" in url:
  82. return url
  83. elif "youtube" in url or "youtu.be" in url:
  84. return url
  85. else:
  86. return None
  87. @staticmethod
  88. async def sub_request(subreddit):
  89. # TODO: Incorperate /random.json for better random results
  90. options = [".json?count=1000", "/top/.json?sort=top&t=all&count=1000"]
  91. choice = random.choice(options)
  92. subreddit += choice
  93. url = "https://reddit.com/r/" + subreddit
  94. try:
  95. r = await roxbot.http.api_request(url)
  96. posts = r["data"]
  97. # This part is to check for some common errors when doing a sub request
  98. # t3 is a post in a listing. We want to avoid not having this instead of a subreddit search, which would be t5.
  99. if not posts.get("after") or posts["children"][0]["kind"] != "t3":
  100. return {}
  101. return posts
  102. except (KeyError, TypeError):
  103. return {}
  104. def cache_refresh(self, cache_id):
  105. # IF ID is not in cache, create cache for ID
  106. if not self.post_cache.get(cache_id, False):
  107. self.post_cache[cache_id] = [("", "")]
  108. def add_to_cache(self, to_cache, cache_id):
  109. self.post_cache[cache_id].append(to_cache)
  110. def cache_clean_up(self, cache_id):
  111. if len(self.post_cache[cache_id]) >= self.cache_limit:
  112. self.post_cache[cache_id].pop(0)
  113. async def random(self, posts, cache_id, nsfw_allowed, loop_amount=20):
  114. """Function to pick a random post of a given list of reddit posts. Using the internal cache.
  115. Returns:
  116. None for failing to get a url that could be posted.
  117. A dict with the key success and the value False for failing the NSFW check
  118. or the post dict if getting the post is successful
  119. """
  120. # Loop to get the post randomly and make sure it hasn't been posted before
  121. url = None
  122. choice = None
  123. for x in range(loop_amount):
  124. choice = random.choice(posts)
  125. url = await self.parse_url(choice["data"]["url"])
  126. if url:
  127. # "over_18" is not a typo. For some fucking reason, reddit has "over_18" for posts, "over18" for subs.
  128. if not nsfw_allowed and choice["data"]["over_18"]:
  129. url = False # Reject post and move to next loop
  130. else:
  131. # Check cache for post
  132. in_cache = False
  133. for cache in self.post_cache[cache_id]:
  134. if url in cache or choice["data"]["id"] in cache:
  135. in_cache = True
  136. if not in_cache:
  137. break
  138. # This is for either a False (NSFW post not allowed) or a None for none.
  139. if url is None:
  140. return {}
  141. elif url is False:
  142. return {"success": False}
  143. # Cache post
  144. self.add_to_cache((choice["data"]["id"], url), cache_id)
  145. # If too many posts in cache, remove oldest value.
  146. self.cache_clean_up(cache_id)
  147. return choice["data"]
  148. class Reddit(commands.Cog):
  149. """The Reddit cog is a cog that allows users to get images and videos from their favourite subreddits."""
  150. SUB_NOT_FOUND = "Error ;-; That subreddit probably doesn't exist. Please check your spelling"
  151. NO_IMAGES = "I couldn't find any images/videos from that subreddit."
  152. NSFW_FAIL = "This channel isn't marked NSFW and therefore I can't post NSFW content. The subreddit given or all posts found are NSFW."
  153. def __init__(self, bot_client):
  154. self.bot = bot_client
  155. self.scrapper = Scrapper()
  156. if not roxbot.imgur_token:
  157. print("REDDIT COG REQUIRES A IMGUR API TOKEN. Without this, roxbot will not return imgur links.")
  158. @commands.command()
  159. @commands.has_permissions(add_reactions=True)
  160. @commands.bot_has_permissions(add_reactions=True)
  161. async def subreddit(self, ctx, subreddit):
  162. """
  163. Grabs an image or video (jpg, png, gif, gifv, webm, mp4) from the subreddit inputted.
  164. Example:
  165. {command_prefix}subreddit pics
  166. """
  167. subreddit = subreddit.lower()
  168. if isinstance(ctx.channel, discord.DMChannel):
  169. cache_id = ctx.author.id
  170. nsfw_allowed = True
  171. else: # Is text channel in guild
  172. cache_id = ctx.guild.id
  173. nsfw_allowed = ctx.channel.is_nsfw()
  174. self.scrapper.cache_refresh(cache_id)
  175. posts = await self.scrapper.sub_request(subreddit)
  176. if not posts:
  177. raise roxbot.UserError(self.SUB_NOT_FOUND)
  178. choice = await self.scrapper.random(posts["children"], cache_id, nsfw_allowed)
  179. if not choice:
  180. raise commands.CommandError(self.NO_IMAGES)
  181. elif choice.get("success", True) is False:
  182. raise roxbot.UserError(self.NSFW_FAIL)
  183. title = "**{}** \nby /u/{} from /r/{}\n".format(unescape(choice["title"]), unescape(choice["author"]), subreddit)
  184. url = str(choice["url"])
  185. if url.split("/")[-2] == "a":
  186. text = "This is an album, click on the link to see more.\n"
  187. else:
  188. text = ""
  189. # Not using a embed here because we can't use video in rich embeds but they work in embeds now :/
  190. output = await ctx.send(title + text + url)
  191. await self.bot.delete_option(output, self.bot.get_emoji(444410658101002261))
  192. if ctx.invoked_with == "subreddit" and isinstance(ctx.channel, discord.TextChannel):
  193. # Only log the command when it is this command being used. Not the inbuilt commands.
  194. await self.bot.log(
  195. ctx.guild,
  196. "subreddit",
  197. User=ctx.author,
  198. Subreddit=subreddit,
  199. Returned="<{}>".format(url),
  200. Channel=ctx.channel,
  201. Channel_Mention=ctx.channel.mention,
  202. Time=roxbot.datetime.format(ctx.message.created_at)
  203. )
  204. @commands.command()
  205. async def aww(self, ctx):
  206. """
  207. Gives you cute pics from reddit
  208. Subreddits: "aww", "redpandas", "lazycats", "rarepuppers", "awwgifs", "adorableart"
  209. """
  210. subreddits = ("aww", "redpandas", "lazycats", "rarepuppers", "awwgifs", "adorableart")
  211. subreddit = random.choice(subreddits)
  212. return await ctx.invoke(self.subreddit, subreddit=subreddit)
  213. @commands.command()
  214. async def feedme(self, ctx):
  215. """
  216. Feeds you with food porn. Uses multiple subreddits.
  217. Yes, I was very hungry when trying to find the subreddits for this command.
  218. Subreddits: "foodporn", "food", "DessertPorn", "tonightsdinner", "eatsandwiches", "steak", "burgers", "Pizza", "grilledcheese", "PutAnEggOnIt", "sushi"
  219. """
  220. subreddits = ("foodporn", "food", "DessertPorn", "tonightsdinner", "eatsandwiches", "steak", "burgers", "Pizza", "grilledcheese", "PutAnEggOnIt", "sushi")
  221. subreddit_choice = random.choice(subreddits)
  222. return await ctx.invoke(self.subreddit, subreddit=subreddit_choice)
  223. @commands.command()
  224. async def feedmevegan(self, ctx):
  225. """
  226. Feeds you with vegan food porn. Uses multiple subreddits.
  227. Yes, I was very hungry when trying to find the subreddits for this command.
  228. Subreddits: "veganrecipes", "vegangifrecipes", "veganfoodporn"
  229. """
  230. subreddits = ["veganrecipes", "vegangifrecipes", "VeganFoodPorn"]
  231. subreddit_choice = random.choice(subreddits)
  232. return await ctx.invoke(self.subreddit, subreddit=subreddit_choice)
  233. @commands.command(aliases=["gssp", "gss", "trans_irl"])
  234. async def traa(self, ctx):
  235. """
  236. Gives you the best trans memes for daysssss
  237. Subreddits: "gaysoundsshitposts", "traaaaaaannnnnnnnnns"
  238. """
  239. subreddits = ("gaysoundsshitposts", "traaaaaaannnnnnnnnns")
  240. subreddit = random.choice(subreddits)
  241. return await ctx.invoke(self.subreddit, subreddit=subreddit)
  242. @commands.command(aliases=["meirl"])
  243. async def me_irl(self, ctx):
  244. """
  245. The full (mostly) me_irl network of subs.
  246. Subreddits: "me_irl", "woof_irl", "meow_irl", "metal_me_irl"
  247. """
  248. subreddits = ("me_irl", "woof_irl", "meow_irl", "metal_me_irl")
  249. subreddit = random.choice(subreddits)
  250. return await ctx.invoke(self.subreddit, subreddit=subreddit)
  251. def setup(bot_client):
  252. bot_client.add_cog(Reddit(bot_client))