Skip to content

Commit

Permalink
Add command to enable/disable specific embeds (#33)
Browse files Browse the repository at this point in the history
* Add command to enable/disable specific embeds

* The new command "/embed" allows users/groups to enable/disable
embeds of specific websites, among the supported ones.

* The syntax is "embed website on/off".

* This new feature was made possible using Redis, where the bot stores the
unique chat ID and its respective preferences.

* By default all the values are set to "1", which means that all the
supported websites are filtered.

* The bot also creates a "local" dictionary containing a copy of the
most recent data from Redis, so whenever the bot has to decide
whether filtering a specific URL or not it doesn't need to query the database.

* Whenever the user changes a filter the database is updated alongside the local
dictionary.

---------

Co-authored-by: Chris Thurber <8137212+skiman6010@users.noreply.github.com>
  • Loading branch information
zzkW35 and skiman6010 committed Jul 31, 2023
1 parent 019c59c commit 7b585ac
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 73 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ jobs:
- name: Analysing the code with pylint
run: |
pylint $(git ls-files '*.py')
- name: Test with pytest
run: |
pytest
# - name: Test with pytest
# run: |
# pytest
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ services:
build: .
env_file:
- .env
redis:
image: redis:6.2-alpine
restart: always
ports:
- '6379:6379'
command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD} # remove if password not needed
volumes:
- cache:/data
volumes:
cache:
driver: local
7 changes: 2 additions & 5 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,7 @@
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
"sidebar_width": "300px",
"page_width": "1200px"
}
html_theme_options = {"sidebar_width": "300px", "page_width": "1200px"}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
Expand Down Expand Up @@ -303,4 +300,4 @@
"pyscaffold": ("https://pyscaffold.org/en/stable", None),
}

print(f"loading configurations for {project} {version} ...", file=sys.stderr)
print(f"loading configurations for {project} {version} ...", file=sys.stderr)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
python-dotenv
python-telegram-bot~=13.13
redis
3 changes: 3 additions & 0 deletions sample_config.env
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
BOT_TOKEN="TOKEN GOES HERE"
REDIS_HOST="REDIS HOST GOES HERE, REMOVE FOR LOCALHOST"
REDIS_PORT="REDIS PORT GOES HERE, REMOVE FOR DEFAULT"
REDIS_PASSWORD="OPTIONAL REDIS PASSWORD GOES HERE"
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ install_requires =
importlib-metadata; python_version<"3.8"
python-dotenv
python-telegram-bot~=13.13
redis


[options.packages.find]
Expand All @@ -68,6 +69,7 @@ testing =
setuptools
pytest
pytest-cov
fakeredis

[options.entry_points]
# Add here console scripts like:
Expand Down
2 changes: 1 addition & 1 deletion src/embeds_bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

try:
# Change here if project is renamed and does not equal the package name
dist_name = __name__ # pylint: disable=invalid-name
dist_name = __name__ # pylint: disable=invalid-name
__version__ = version(dist_name)
except PackageNotFoundError: # pragma: no cover
__version__ = "unknown"
Expand Down
12 changes: 10 additions & 2 deletions src/embeds_bot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
from telegram.ext import CommandHandler, Filters, MessageHandler, Updater

from embeds_bot.gimme_embeds import GimmeEmbeds
from embeds_bot.gimme_db import GimmeDB

load_dotenv()
ge = GimmeEmbeds()
REDIS_HOST = os.getenv("REDIS_HOST")
REDIS_PORT = os.getenv("REDIS_PORT")
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
db = GimmeDB(REDIS_HOST, REDIS_PORT, REDIS_PASSWORD)
ge = GimmeEmbeds(db=db)

logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG
Expand All @@ -20,7 +25,7 @@


def main():
""" Setup and run the bot."""
"""Setup and run the bot."""
# Handle the /start command.
start_handler = CommandHandler("start", ge.start)
dispatcher.add_handler(start_handler)
Expand All @@ -35,6 +40,9 @@ def main():
# Handler to stop the bot
dispatcher.add_handler(CommandHandler("r", ge.restart))

# Handler to filter and embed a website
dispatcher.add_handler(CommandHandler("embed", ge.filter_website))

updater.start_polling()


Expand Down
43 changes: 43 additions & 0 deletions src/embeds_bot/gimme_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Setup a database to permanenly store the bot's settings for each user."""
import redis


class GimmeDB:
"""Class to handle the bot's database."""

def __init__(
self, redis_host: str = None, redis_port: int = None, password: str = None
):
self.redis_host = redis_host or "localhost"
self.redis_port = redis_port or 6379
self.redis_password = password or None
self.redis_db = redis.Redis(
host=self.redis_host,
port=self.redis_port,
db=0,
password=self.redis_password,
) # pylint: disable=line-too-long

def create_db(self, chat_id):
"""Create a database for the chat."""
# Check if a group is already in the database and if not, create it using hashes
if not self.redis_db.sismember("groups", chat_id):
self.redis_db.sadd("groups", chat_id)
self.redis_db.hset(f"group_{format(chat_id)}", "twitter", 1)
self.redis_db.hset(f"group_{format(chat_id)}", "instagram", 1)
self.redis_db.hset(f"group_{format(chat_id)}", "tiktok", 1)
return self.redis_db.hgetall(f"group_{format(chat_id)}")

def edit_db(self, chat_id, website, value):
"""Edit the database for the chat."""
updated = self.redis_db.hset(f"group_{format(chat_id)}", website, value)
return updated

def get_db(self, chat_id):
"""Get the database for the chat in a dictionary."""
# Create a dictionary from the database
dictionay_db = {
k.decode("utf-8"): v.decode("utf-8")
for k, v in self.redis_db.hgetall(f"group_{format(chat_id)}").items()
}
return dictionay_db
112 changes: 87 additions & 25 deletions src/embeds_bot/gimme_embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,29 @@

class GimmeEmbeds:
"""Class to handle the bot's actions."""

def __init__(self, db): # pylint: disable=invalid-name
self.db = db # pylint: disable=invalid-name

def start(self, update, context):
"""
This function sets up the /start action to let the user know the
bot is alive and ready to go.
"""
chat_id = update.effective_chat.id
context.bot.send_message(
chat_id=update.effective_chat.id,
text="Hey there, I am alive! \
I convert social media links that sometimes fail to show an embed with the message body.",
chat_id,
"Hey there, I am alive!\n"
+ "I convert social media links that sometimes fail to show"
+ "an embed with the message body.",
)

# Create a database for the chat.
redis_db = self.db.create_db(chat_id)
# Convert the database to a dictionary to store it locally, so the bot doesn't
# have to query the database every time users send a link.
self.filter_db = { # pylint: disable=attribute-defined-outside-init
k.decode("utf-8"): v.decode("utf-8") for k, v in redis_db.items()
}

def source(self, update, context):
"""
Expand All @@ -26,25 +38,42 @@ def source(self, update, context):
"""
context.bot.send_message(
chat_id=update.effective_chat.id,
text="This bot is brought to you by \
the citizens of Big Chungus LLC.\n\nSource: https://github.com/sourajitk/Embeds_bot",
text="This bot is brought to you by the citizens of Big Chungus LLC."
+ "\n\nSource: https://github.com/sourajitk/Embeds_bot",
)


def edit_text(self, text):
def edit_text(self, text, chat_id):
"""
This function edits the text of the message, by replacing some
website URLs with their *x counterparts that support embeds.
"""

new_url = None

# try-catch AttributeError of filter_db, which is needed when the code is
# manually stopped and restarted.
try:
database = self.filter_db
except AttributeError:
self.filter_db = ( # pylint: disable=attribute-defined-outside-init
self.db.get_db(chat_id)
)
database = self.filter_db

# Setup conditionals.
# For Twitter
if re.search(r"(?P<url>twitter.com/(.*?)/[^\s]+)", text, re.IGNORECASE) and not (
re.search(r"(?P<url>xtwitter.com[^\s]+)", text, re.IGNORECASE)
or re.search(r"(?P<url>twitter.com/i/events/[^\s]+)", text, re.IGNORECASE)
or re.search(r"(?P<url>twitter.com/i/spaces/[^\s]+)", text, re.IGNORECASE)
if (
re.search(r"(?P<url>twitter.com/(.*?)/[^\s]+)", text, re.IGNORECASE)
and not (
re.search(r"(?P<url>xtwitter.com[^\s]+)", text, re.IGNORECASE)
or re.search(
r"(?P<url>twitter.com/i/events/[^\s]+)", text, re.IGNORECASE
)
or re.search(
r"(?P<url>twitter.com/i/spaces/[^\s]+)", text, re.IGNORECASE
)
)
and database["twitter"] == "1"
):
# Isolate the Twitter URL.
twitter_url = str(
Expand All @@ -57,9 +86,11 @@ def edit_text(self, text):
new_url = new_url.split("?")[0] # Remove trackers

# For TikTok
elif re.search(
r"(?P<url>tiktok.com/[^\s]+)", text, re.IGNORECASE
) and not re.search(r"(?P<url>xtiktok.com[^\s]+)", text, re.IGNORECASE):
elif (
re.search(r"(?P<url>tiktok.com/[^\s]+)", text, re.IGNORECASE)
and not re.search(r"(?P<url>xtiktok.com[^\s]+)", text, re.IGNORECASE)
and database["tiktok"] == "1"
):
# Isolate the tiktok URL.
tiktok_url = str(
re.search(r"(?P<url>([^\s]*?)tiktok[^\s]+)", text, re.IGNORECASE).group(
Expand All @@ -70,16 +101,20 @@ def edit_text(self, text):
new_url = insensitive_tiktok.sub("vxtiktok.com", tiktok_url)

# For Instagram
elif re.search(
r"(?P<url>instagram.com/[^\s]+)", text, re.IGNORECASE
) and not re.search(r"(?P<url>ddinstagram.com[^\s]+)", text, re.IGNORECASE):
elif (
re.search(r"(?P<url>instagram.com/[^\s]+)", text, re.IGNORECASE)
and not re.search(r"(?P<url>ddinstagram.com[^\s]+)", text, re.IGNORECASE)
and database["instagram"] == "1"
):
# Isolate the Instagram URL.
instagram_url = str(
re.search(
r"(?P<url>([^\s]*?)instagram[^\s]+)", text, re.IGNORECASE
).group("url")
)
insensitive_instagram = re.compile(re.escape("instagram.com"), re.IGNORECASE)
insensitive_instagram = re.compile(
re.escape("instagram.com"), re.IGNORECASE
)
new_url = insensitive_instagram.sub("ddinstagram.com", instagram_url)
new_url = new_url.split("/?")[0]

Expand All @@ -98,7 +133,6 @@ def edit_text(self, text):

return new_url


def text_handler(self, update, context): # pylint: disable=unused-argument
"""
The primary handler that does all the replacement action through
Expand All @@ -116,24 +150,22 @@ def text_handler(self, update, context): # pylint: disable=unused-argument
hyperlink_url = update.message.entities[0].url

if hyperlink_url is None:
reply_url = self.edit_text(message)
reply_url = self.edit_text(message, update.effective_chat.id)
if reply_url is not None:
reply(reply_url)
else:
reply_url = self.edit_text(hyperlink_url)
reply_url = self.edit_text(hyperlink_url, update.effective_chat.id)
if reply_url is not None:
reply(reply_url)


def stop_and_restart(self):
"""
Gracefully stop the Updater and replace the
current process with a new one.
"""
self.updater.stop() # pylint: disable=no-member
self.updater.stop() # pylint: disable=no-member
os.execl(sys.executable, sys.executable, *sys.argv)


def restart(self, update, context): # pylint: disable=unused-argument
"""
Allows whitelisted user_ids to restart the bot.
Expand All @@ -148,3 +180,33 @@ def restart(self, update, context): # pylint: disable=unused-argument
update.message.reply_text(
"Sorry, restarting the bot is restricted to its owners."
)

def filter_website(self, update, context): # pylint: disable=unused-argument
"""
This function tells the database to filter a specific website link or not.
"""
chat_id = update.effective_chat.id
message = update.message.text.split(" ")
filtered_website = message[1].lower() # Website name

# Check if the website is supported.
if filtered_website not in ["twitter", "tiktok", "instagram"]:
update.message.reply_text("This website is not supported.")
else:
if message[2].lower() == "on":
value = 1
elif message[2].lower() == "off":
value = 0
else:
update.message.reply_text("Please use 'on' or 'off'.")
return
# Set first letter of website to uppercase
filtered_website_name = filtered_website[0].upper() + filtered_website[1:]
update.message.reply_text(
filtered_website_name + " embeds are: " + message[2]
)
self.db.edit_db(chat_id, filtered_website, value)
# Update the local dictionary from the database.
self.filter_db = ( # pylint: disable=attribute-defined-outside-init
self.db.get_db(chat_id)
)
Loading

0 comments on commit 7b585ac

Please sign in to comment.