Skip to content

Commit

Permalink
Merge pull request #2 from amadejkastelic/next
Browse files Browse the repository at this point in the history
Lib bump, refactoring, slash command
  • Loading branch information
amadejkastelic authored Jul 3, 2024
2 parents e37fcd2 + cd4a5c1 commit 1e35c5a
Show file tree
Hide file tree
Showing 14 changed files with 673 additions and 551 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ inline-quotes = single
multiline-quotes = single
docstring-quotes = double
ban-relative-imports = true
application-import-names = downloader,models,utils
application-import-names = downloader,models,utils,bots
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim-bookworm
FROM python:3.12-slim-bookworm

ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1
Expand All @@ -24,6 +24,7 @@ COPY Pipfile.lock ./
COPY *.py ./
COPY downloader/* ./downloader/
COPY models/* ./models/
COPY bots/* ./bots/

RUN pipenv install && pipenv run playwright install chromium && pipenv run playwright install-deps

Expand Down
19 changes: 10 additions & 9 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ name = "pypi"
[packages]
tiktokapipy = "==0.2.4.post2"
playwright = "==1.43.0"
"discord.py" = "==2.3.2"
instaloader = "==4.11"
"discord.py" = "==2.4.0"
instaloader = "==4.12"
facebook-scraper = "==0.2.59"
python-magic = "0.4.27"
redvid = "==2.0.4"
redvid = "==2.0.5"
ffmpeg-python = "==0.2.0"
opencv-python = "==4.9.0.80"
opencv-python = "==4.10.0.84"
asyncpraw = "==7.7.1"
twscrape = "==0.12"
pytube = "==15.0.0"
twscrape = "==0.13"
pytube2 = "==15.0.6"
lxml-html-clean = "==0.1.1"

[dev-packages]
black = "==24.4.2"
flake8 = "==7.0.0"
flake8-import-order = "==0.18.2 "
flake8 = "==7.1.0"
flake8-import-order = "==0.18.2"

[requires]
python_version = "3.11"
python_version = "3.12"
869 changes: 438 additions & 431 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A Discord bot that automatically embeds media and metadata of messages containin
```bash
docker pull ghcr.io/amadejkastelic/discord-video-embed-bot:<latest|tag>
```
- Run it with your discord api key: `docker run -e DISCORD_API_KEY=<api_key> video-embed-bot`
- Run it with your discord api key: `docker run -e DISCORD_API_TOKEN=<api_token> video-embed-bot`

### Facebook
Facebook requires you to provide cookies. Download them in your browser using [an extension](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) while you're logged in and mount them to the container).
Expand Down
Empty file added bots/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions bots/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import typing

import constants


class BaseBot(object):
TYPE: constants.BotType

def __init__(self, api_token: str) -> None:
self.api_token = api_token

async def run(self) -> typing.NoReturn:
raise NotImplementedError()
Empty file added bots/discord/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions bots/discord/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import typing

import discord

from bots import base
from bots.discord import client


class DiscordBot(base.BaseBot):
def __init__(self, api_token: str) -> None:
super().__init__(api_token)

intents = discord.Intents.default()
intents.message_content = True

self.client = client.DiscordClient(intents=intents)

async def run(self) -> typing.NoReturn:
await self.client.start(token=self.api_token)
153 changes: 153 additions & 0 deletions bots/discord/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import asyncio
import datetime
import logging
import typing
from functools import partial

import discord
from discord import app_commands
from discord import ui

import models
import utils
from downloader import registry


class CustomView(ui.View):
@ui.button(label='❌')
async def on_click(
self,
interaction: discord.Interaction,
button: ui.Button,
) -> None:
await interaction.message.delete()


class DiscordClient(discord.Client):
def __init__(self, *, intents: discord.Intents, **options: typing.Any) -> None:
super().__init__(intents=intents, **options)

self.tree = app_commands.CommandTree(client=self)
self.tree.add_command(
app_commands.Command(
name='embed',
description='Embeds media directly into discord',
callback=self.command_embed,
)
)

async def on_ready(self):
await self.tree.sync()
logging.info(f'Logged on as {self.user}')

async def on_message(self, message: discord.Message):
if message.author == self.user:
return

url = utils.find_first_url(message.content)
if not url:
return

try:
client = registry.get_instance(url=url)
except Exception as e:
logging.error(f'Failed to obtain a strategy for url {url}. Error: {str(e)}')
return

new_message = (await asyncio.gather(message.delete(), message.channel.send('🔥 Working on it 🥵')))[1]

try:
post = await client.get_post()
except Exception as e:
logging.error(f'Failed downloading {url}: {str(e)}')
await asyncio.gather(
new_message.edit(content=f'Failed downloading {url}. {message.author.mention}'),
new_message.add_reaction('❌'),
)
raise e

try:
msg = await self._send_post(post=post, send_func=message.channel.send, author=message.author)
logging.info(f'User {message.author.display_name} sent message with url {url}')
except Exception as e:
logging.error(f'Failed sending message {url}: {str(e)}')
msg = await message.channel.send(
content=f'Failed sending discord message for {url} ({message.author.mention}).\nError: {str(e)}'
)

await asyncio.gather(msg.add_reaction('❌'), new_message.delete())

async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User):
if (
reaction.message.author.id == self.user.id
and reaction.emoji == '❌'
and user.mentioned_in(message=reaction.message)
and reaction.message.created_at.replace(tzinfo=None)
>= (datetime.datetime.utcnow() - datetime.timedelta(minutes=5))
):
logging.info(f'User {user.display_name} deleted message {utils.find_first_url(reaction.message.content)}')
await reaction.message.delete()

async def command_embed(self, interaction: discord.Interaction, url: str, spoiler: bool = False) -> None:
await interaction.response.defer()

try:
client = registry.get_instance(url=url)
except Exception as e:
logging.error(f'Failed to obtain a strategy for url {url}. Error: {str(e)}')
return

try:
post = await client.get_post()
if not post.spoiler:
post.spoiler = spoiler
except Exception as e:
logging.error(f'Failed downloading {url}: {str(e)}')
await interaction.followup.send(f'Failed fetching {url} ({interaction.user.mention}).\nError: {str(e)}')
raise e

await self._send_post(
post=post,
send_func=partial(interaction.followup.send, view=CustomView()),
author=interaction.user,
)

async def _send_post(
self,
post: models.Post,
send_func: typing.Callable,
author: discord.User,
) -> discord.Message:
send_kwargs = {
'suppress_embeds': True,
}
file = None
if post.buffer:
extension = utils.guess_extension_from_buffer(buffer=post.buffer)
file = discord.File(
fp=post.buffer,
filename='{spoiler}file{extension}'.format(
spoiler='SPOILER_' if post.spoiler else '',
extension=extension,
),
)
send_kwargs['file'] = file

try:
content = f'Here you go {author.mention} {utils.random_emoji()}.\n{str(post)}'
if len(content) > 2000:
if post.spoiler:
content = content[:1995] + '||...'
else:
content = content[:1997] + '...'

send_kwargs['content'] = content

return await send_func(**send_kwargs)
except discord.HTTPException as e:
if e.status != 413: # Payload too large
raise e
logging.info('File too large, resizing...')
file.fp.seek(0)
post.buffer = await utils.resize(buffer=file.fp, extension=extension)
return await self._send_post(post=post, send_func=send_func, author=author)
15 changes: 15 additions & 0 deletions bots/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
import typing

from bots.discord import bot


async def run_strategies() -> typing.NoReturn:
"""
TODO: Add support for multiple strategies and move env vars to settings/config
"""
discord_api_key = os.environ.get('DISCORD_API_TOKEN')
if discord_api_key:
await bot.DiscordBot(api_token=discord_api_key).run()
else:
raise RuntimeError('DISCORD_API_TOKEN environment variable not set, plesae set it to a valid value.')
5 changes: 5 additions & 0 deletions constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import enum


class BotType(enum.Enum):
DISCORD = 'discord'
117 changes: 9 additions & 108 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,116 +1,17 @@
import asyncio
import datetime
import logging
import os
import random

import discord
from bots import registry

import models
import utils
from downloader import registry

emoji = ['😼', '😺', '😸', '😹', '😻', '🙀', '😿', '😾', '😩', '🙈', '🙉', '🙊', '😳']
async def main():
await registry.run_strategies()


class DiscordClient(discord.Client):
async def on_ready(self):
logging.info(f'Logged on as {self.user}')
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
)

async def on_message(self, message: discord.Message):
if message.author == self.user:
return

url = utils.find_first_url(message.content)
if not url:
return

try:
client = registry.get_instance(url=url)
except Exception:
return

new_message = (await asyncio.gather(message.delete(), message.channel.send('🔥 Working on it 🥵')))[1]

try:
post = await client.get_post()
except Exception as e:
logging.error(f'Failed downloading {url}: {str(e)}')
await asyncio.gather(
new_message.edit(content=f'Failed downloading {url}. {message.author.mention}'),
new_message.add_reaction('❌'),
)
raise e

try:
msg = await self._send_post(post=post, channel=message.channel, author=message.author)
logging.info(f'User {message.author.display_name} sent message with url {url}')
except Exception as e:
logging.error(f'Failed sending message {url}: {str(e)}')
msg = await message.channel.send(
content=f'Failed sending discord message for {url} ({message.author.mention}).\nError: {str(e)}'
)

await asyncio.gather(msg.add_reaction('❌'), new_message.delete())

async def _send_post(
self, post: models.Post, channel: discord.GroupChannel, author: discord.User
) -> discord.Message:
file = None
if post.buffer:
extension = utils.guess_extension_from_buffer(buffer=post.buffer)
file = discord.File(
fp=post.buffer,
filename='{spoiler}file{extension}'.format(
spoiler='SPOILER_' if post.spoiler else '',
extension=extension,
),
)

try:
content = f'Here you go {author.mention} {random.choice(emoji)}.\n{str(post)}'
if len(content) > 2000:
if not post.spoiler:
content = content[:1997] + '...'
else:
content = content[:1995] + '||...'

return await channel.send(
content=content,
file=file,
suppress_embeds=True,
)
except discord.HTTPException as e:
if e.status != 413: # Payload too large
raise e
logging.info('File too large, resizing...')
file.fp.seek(0)
post.buffer = await utils.resize(buffer=file.fp, extension=extension)
return await self._send_post(post=post, channel=channel, author=author)

async def on_reaction_add(self, reaction: discord.Reaction, user: discord.User):
if (
reaction.message.author.id == self.user.id
and reaction.emoji == '❌'
and user.mentioned_in(message=reaction.message)
and reaction.message.created_at.replace(tzinfo=None)
>= (datetime.datetime.utcnow() - datetime.timedelta(minutes=5))
):
logging.info(f'User {user.display_name} deleted message {utils.find_first_url(reaction.message.content)}')
await reaction.message.delete()


logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
)

intents = discord.Intents.default()
intents.message_content = True
client = DiscordClient(intents=intents)

api_key = os.environ.get('DISCORD_API_TOKEN')
if not api_key:
raise RuntimeError('DISCORD_API_TOKEN environment variable not set, plesae set it to a valid value and try again.')

client.run(api_key)
asyncio.run(main())
Loading

0 comments on commit 1e35c5a

Please sign in to comment.