Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bots #105

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eb0a7ca
init bots
eric-volz Jul 22, 2023
ef2c983
update api endpoint
eric-volz Jul 26, 2023
5db8ae9
new profile picture
eric-volz Jul 29, 2023
9169342
fix user token generation?
eric-volz Jul 29, 2023
2e3adf2
implement starting message in private chats
eric-volz Jul 29, 2023
89557e6
implement answer on replied message
eric-volz Jul 29, 2023
ae4c8f7
implement logging instead of print
eric-volz Jul 29, 2023
b10248d
.env template for bots
eric-volz Jul 30, 2023
2e0f06e
readme for bots
eric-volz Jul 30, 2023
3a025a9
remove /private from discord bot
eric-volz Jul 30, 2023
4453cf3
change picture position and size in readme
eric-volz Jul 30, 2023
ad491ac
uncomment 2000 character prompt
eric-volz Jul 30, 2023
3762d79
temporary api fix?
eric-volz Jul 30, 2023
545beb2
implement answer on replied message: discord
eric-volz Jul 31, 2023
bd29081
return api connection to json
eric-volz Jul 31, 2023
78bc4cc
implement logging instead of print statements
eric-volz Jul 31, 2023
281c2ab
discord: load first message
eric-volz Jul 31, 2023
8294996
missing heading
eric-volz Jul 31, 2023
d06313f
add logger to discord docker file
eric-volz Jul 31, 2023
67fc1d2
Add fly toml for telegram bot deployment
0ptim Aug 5, 2023
f2de4bd
Add action for telegram bot deployment
0ptim Aug 5, 2023
6cc054a
Fix wrong fly deployment token
0ptim Aug 5, 2023
5cc7dbc
Remove `app` from fly files
0ptim Aug 5, 2023
6a98271
Fix telegram deployment
0ptim Aug 5, 2023
637c772
Remove stopping if idle
0ptim Aug 5, 2023
1fd7a31
Add action for production telegream bot
0ptim Aug 5, 2023
5271fd9
Adjust workflow trigger for production
0ptim Aug 5, 2023
7e6df09
add disclaimer to telegram answer
eric-volz Sep 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/fly_telegram_production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Fly.io Telegram Bot Production

on:
pull_request:
branches:
- main
workflow_dispatch:

env:
FLY_API_TOKEN: ${{ secrets.PRODUCTION_FLY_TELEGRAM_DEPLOY_TOKEN }}

jobs:
deploy:
name: Deploy production telegram bot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy bot
run: flyctl deploy --remote-only -a jellychat-telegram -c fly_telegram.toml
working-directory: bots
20 changes: 20 additions & 0 deletions .github/workflows/fly_telegram_staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Fly.io Telegram Bot Staging

on:
push:
branches:
- main

env:
FLY_API_TOKEN: ${{ secrets.STAGING_FLY_TELEGRAM_DEPLOY_TOKEN }}

jobs:
deploy:
name: Deploy staging telegram bot
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy bot
run: flyctl deploy --remote-only -a jellychat-telegram-staging -c fly_telegram.toml
working-directory: bots
1 change: 0 additions & 1 deletion backend/fly.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
app = "jellychat"
kill_signal = "SIGINT"
kill_timeout = "5s"

Expand Down
4 changes: 4 additions & 0 deletions bots/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
TELEGRAM_BOT_USERNAME=telegram_bot_username
TELEGRAM_TOKEN=telegram_token

DISCORD_TOKEN=discord_token
95 changes: 95 additions & 0 deletions bots/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# JellyChat Bots

## Bot Links

- [Telegram](http://t.me/DefichainJellyChatBot)
- [Discord](https://discord.com/api/oauth2/authorize?client_id=1132019055805599908&permissions=68608&scope=bot)

### Permissions

- Telegram
- Needs to be administrator inside a **group** to work properly
- Discord (Permissions Integer 68608):
- Send Messages
- Read Messages / View Channels
- Read Message History

## Developer Interface Links

- Telegram: http://t.me/BotFather
- Discord: https://discord.com/developers/applications

## Bot Description

1. Name: JellyChat
2. About Text: Jelly Chat 🪼 GPT Bot with information about Defichain
3. Description: Hey there!🪼 If you have any question about Defichain, don't be shy and ask me something⁉️
4. User Picture:

<img src="profile_picture.svg" alt="Bild" width="300">

## Bot Commands:

- **/start**
- when it is used for the first time, the initial welcome message will be loaded
- otherwise a standard prompted will be sent to the user

## Environment Variables
- `TELEGRAM_BOT_USERNAME`
- telegram username without an @ at the beginning
- `TELEGRAM_TOKEN`
- telegram token generated by [@BotFather](http://t.me/BotFather)
- `DISCORD_TOKEN`
- telegram token generated within [developer interface](https://discord.com/developers/applications)

## Run Bots

### Telegram Bot

#### Python

Python dependencies:
```bash
pip install -r telegram/requirements.txt
```

Python run:
```bash
python3 telegram/telegram_bot.py
```

#### Docker

Docker build:
```bash
docker build -t jelly_chat_telegram_bot -f telegram/Dockerfile .
```
Docker run:
```bash
docker run --name JellyChatTelegramBot --env-file .env -d jelly_chat_telegram_bot
```

### Discord Bot

#### Python

Python dependencies:
```bash
pip install -r discord/requirements.txt
```

Python run:
```bash
python3 discord/discord_bot.py
```

#### Docker

Docker build:
```bash
docker build -t jelly_chat_discord_bot -f discord/Dockerfile .
```
Docker run:
```bash
docker run --name JellyChatDiscordBot --env-file .env -d jelly_chat_discord_bot
```
21 changes: 21 additions & 0 deletions bots/discord/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Python Version
FROM python:3.10.12

# Create Docker User
RUN useradd --create-home appuser
WORKDIR /home/appuser

# Copy Files
COPY jellychatapi.py .
COPY logger.py .
COPY discord/ .

# Install Dependencies
RUN pip install --no-cache-dir -r requirements.txt \
&& chown -R appuser:appuser .

# Change User
USER appuser

# Start Script
CMD ["python", "discord_bot.py"]
Empty file added bots/discord/__init__.py
Empty file.
144 changes: 144 additions & 0 deletions bots/discord/discord_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import os
import sys
import logging
from dotenv import load_dotenv

import discord

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")))
from jellychatapi import JellyChatAPI
from logger import configure_logging


configure_logging()


class JellyChatDiscordBot(discord.Client):
APPLICATION: str = "discord"

def __init__(self):
self.jellyChatAPI = JellyChatAPI()

logging.info("Initializing bots...")
intents = discord.Intents.default()
intents.message_content = True
super().__init__(intents=intents)

async def on_ready(self) -> None:
"""
This method will log if the bot has stared successfully and list all connected guilds
"""
logging.info("The Discord bot is up and running...")
logging.info("The Discord Bot is running on the following servers: ")
for guild in self.guilds:
logging.info(f"\t{self.guilds.index(guild)}: {guild.name}")

async def on_message(self, message):
"""
This method handles all incoming messages for the bot:
It will whether the message is coming from a private or a guild chat and acts accordingly
"""
# Ignore message from the bot himself
if message.author == self.user:
return

# Distinguish between private and guild chat
if isinstance(message.channel, discord.DMChannel):
await self.private(message)
else:
await self.guild(message)

async def guild(self, message):
"""
This method handles all messages of a guild chat:
The Bot only answers if he is tagged
"""
# Only generates and sends message if bot is tagged
reply = None
if message.reference:
reply = await self.fetch_message(message, message.reference.message_id)

if self.user in message.mentions:
answer: str = self.get_answer(message, is_private=False, reply=reply)
await self.send(message, answer, is_private=False)

async def private(self, message):
"""
This method handles all messages of a private chat:
It will always answer on anything that has been written into the chat
"""
await self.is_first_message(message)
answer: str = self.get_answer(message, is_private=True)
await self.send(message, answer, is_private=True)

def get_answer(self, message, is_private: bool, reply=None):
"""
This method queries an answer for the asked question
"""
# Private chat identification: author name
# Guild chat identification: guild name + channel name
userToken: str = JellyChatAPI.create_user_token(message.guild.name + message.channel.name
if not is_private else message.author.name)

# Prompt that is appended to the question.
extra_prompt = " The answer must be less than 2000 characters in length."

message_content: str = message.content.replace(f"<@{self.user.id}>", "") # + extra_prompt

if reply:

reply_content: str = reply.content.replace(f"<@{self.user.id}>", "")
logging.info(f"Question: User: {message.author.name}, Reply:\n{reply_content}\nQuestion:\n{message_content}")
question: str = f"{reply_content}\n\n{message_content}"
else:
question: str = message_content
logging.info(f"Question: User: {message.author.name}, Question:\n{message_content}")

answer: str = self.jellyChatAPI.user_message(userToken, question, JellyChatDiscordBot.APPLICATION)
logging.info(f"Answer: User: {message.author.name}, Answer: {answer}")
return answer

async def send(self, message, answer, is_private: bool):
"""
This method sends the answer either into the guild or private chat
"""
try:
if is_private:
await message.author.send(answer, reference=message)
else:
await message.channel.send(answer, reference=message)
except Exception as e: # An error will occur if the answer is longer than 2000 characters
logging.error(f"There is an error sending the message: {e}")
msg = "Woooops, something went wrong while trying to ship the answer to you."
if is_private:
await message.author.send(msg, reference=message)
else:
await message.channel.send(msg, reference=message)

async def is_first_message(self, message):
userToken: str = JellyChatAPI.create_user_token(message.author.name)
history = self.jellyChatAPI.user_history(userToken)
# If new user --> load context user message
if len(history) == 1:
logging.info(f"New User: {message.author.name}")
await message.author.send(history[0].get("content"))

async def fetch_message(self, ctx, message_id: int):
try:
# Fetch the message from the channel using the message_id
message = await ctx.channel.fetch_message(message_id)
return message
except discord.NotFound:
logging.error("Message not found.")
except discord.Forbidden:
logging.error("Bot doesn't have permission to access the message.")
except discord.HTTPException:
logging.error("An error occurred during the HTTP request.")


if __name__ == '__main__':
load_dotenv()
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")

app = JellyChatDiscordBot()
app.run(DISCORD_TOKEN)
3 changes: 3 additions & 0 deletions bots/discord/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
discord
python-dotenv~=1.0.0
requests~=2.31.0
11 changes: 11 additions & 0 deletions bots/fly_telegram.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
primary_region = "ams"

[build]
dockerfile = "telegram/Dockerfile"

[http_service]
internal_port = 8080
force_https = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
42 changes: 42 additions & 0 deletions bots/jellychatapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import requests
import hashlib
import logging
from typing import Any


class JellyChatAPI:

DISCLAIMER = "Disclaimer:\nThis message is generated by JellyChat, an defichain specific chatbot. It's not yet " \
"perfect, so take every message with a pinch of salt!🧂"

@staticmethod
def create_user_token(identification: Any) -> str:
return hashlib.sha256((str(identification)).encode("utf-8")).hexdigest()

@staticmethod
def get_response(response):
if "response" in response:
return response.get("response")
else:
logging.error(f"There is an error with the connection to JellyChat API")
return f"Hey here is Jelly. I'm sleeping right now!💤"

def __init__(self, url: str = "https://jellychat.fly.dev"):
self.url = url

def user_message(self, userToken: str, message: str, application):
try:
self.user_history(userToken)
return JellyChatAPI.get_response(requests.post(self.url + "/user_message",
json={"user_token": userToken, "application": application,
"message": message}).json())
except Exception as e:
logging.error(f"Failing connection to API {self.url}: \n{e}")
return f"Hey here is Jelly. I'm sleeping right now!💤"

def user_history(self, userToken: str) -> [{}]:
return requests.post(self.url + "/history", json={"user_token": userToken}).json()


if __name__ == "__main__":
pass
18 changes: 18 additions & 0 deletions bots/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging


def configure_logging():
# Erstelle einen Logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Erstelle einen StreamHandler, um die Ausgaben in den Standardausgabestrom (stdout) zu leiten
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)

# Definiere ein Format für die Log-Einträge
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(formatter)

# Füge den StreamHandler zum Logger hinzu
logger.addHandler(stream_handler)
Loading