-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# .env | ||
DISCORD_TOKEN= | ||
BB_NAME= | ||
BB_PASS= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
|
||
# Birdini BirdBuddy Discord Companion | ||
|
||
Birdini is a Discord bot that utilises post notifications, images, and videos from your BirdBuddy into a Discord channel, right as the magic happens. Share all those wonderful bird sitings with your community and friends automatically! | ||
|
||
|
||
![alttext](https://imgur.com/AbA50Py.png) | ||
|
||
|
||
|
||
|
||
|
||
## Commands | ||
|
||
| Command | Arguments | Description | | ||
| :-------- | :------- | :------------------------- | | ||
| `/channel` | `channel` or `channelID` | Set which channel you want Birdini to post noitifications to. | | ||
|`/muted_channel`| `channel` or `channelID` | Set which channel you want Birdini to post notifications to when muted (private channel recommended).| | ||
|`/mute` | `None`| Birdini will no longer post messages in the primary channel and send them to the defined muted channel instead. | ||
|`/unmute`|`None`| Birdini will continue to post messages in the primary channel defined. | ||
|
||
|
||
## Environment Variables | ||
|
||
To run this project, you will need to add the following environment variables to your .env file or use the template provided in the Git. | ||
|
||
`DISCORD_TOKEN` | ||
Discord bot token that can be gotten from https://discord.com/developers. | ||
|
||
`BB_NAME` | ||
This is the email used to log into your BirdBuddy account. Note that this has to be an email/password login and can not be an integration like login with Google. A good work around for this issue (if your primary account is using such type of login). Is to create a new account using an email and then inviting them to be a guest at your feeder. | ||
|
||
`BB_PASS` | ||
This is the password used to log into your BirdBuddy account. | ||
|
||
# Setup Guide | ||
This makes use of the [pybirdbuddy](https://github.com/jhansche/pybirdbuddy/releases/tag/v0.0.14) library for API calls. | ||
|
||
### Files | ||
How to aquire and arrange all the files needed in order to get Birdini to work. | ||
- [Download](https://github.com/jhansche/pybirdbuddy/releases/tag/v0.0.14) the pybirdbuddy library. Birdini was built using v0.0.14. Newer versions may work but v0.0.14 is recommended. | ||
- Create a new folder, I will be calling mine Birdini Bot for the sake of this guide. | ||
- Place the pybirdbuddy-0.0.14 folder inside your new one and rename it `pybirdbuddy`. | ||
- Download the files from this Git and place them in your Birdini Bot folder. | ||
- You should then end up with a folder looking like this: ![alttext](https://i.imgur.com/jbLGL7q.png) | ||
- Insert the appropriate information into your .env file. Do not worry yet about the `DISCORD_TOKEN` as we will be getting this next. | ||
|
||
|
||
### Discord Bot | ||
|
||
A brief guide on how to create a Discord bot and obtain your `DISCORD_TOKEN`. | ||
- Go to https://discord.com/developers/applications | ||
- Click "New Application" in the top right and name your bot. | ||
- Go to the "Bot" section, disable "PUBLIC BOT" and enable all other settings. | ||
- Press "Reset Token" towards the top of the page and copy your new token. | ||
- Add the copied token to your .env file. It should look like this `DISCORD_TOKEN=1234567890` | ||
- Go to the "OAuth2" section and select "URL Generator". | ||
- Select the "bot" tickbox and then the "Administrator" tickbox that will appear in the new interface below. | ||
- Copy the link generated at the bottom, paste it into your browser, and add the bot to your server. | ||
|
||
### Setting up AWS (free) | ||
How to setup and host your bot 24/7, for free using AWS! | ||
- Go to https://aws.amazon.com/ | ||
- Press the "Sign in to the console" button in the top right. | ||
- You will be redirected to a login screen. Log in or create your account. | ||
- On the "Console Home" page you can scroll down and see a "Build a solution" header. | ||
- Under this header you will see "Launch a virtual machine (With EC2)", select this. | ||
- Ensure that the Application and OS Images are set to "Amazon Linux" (it should be this by default but check anyway). | ||
- Scroll down further until you see "Key pair (login)". Select "Create a new key pair". | ||
- Name this whatever you like and leave it on the default settings. | ||
- When this is all done press the "Launch instance" button on the right side. | ||
- At the very top left of the screen you can click the word "Instances" to be taken to your list of instances. | ||
- It may take some time to setup, just wait until Instance state says "Running" and the status checks have passed. | ||
|
||
Now the instance is created we are going to need to upload our files and install some Python dependencies. We will need some additional software to easily upload our files to the AWS. I will be using FileZilla for this guide which can be found [here](https://filezilla-project.org/). | ||
|
||
- We are going to need to connect to our server using FileZilla. To see the information we are going to need we should click the string of bluee text under Instance ID. | ||
- Now open FileZilla and select Edit > Settings | ||
- Locate the "SFTP" section and select "Add key file". This is what we download earlier when we created a new key pair. | ||
- Once this has been selected we can exit out of the settings menu and navigate to File > Site Manager. | ||
- Press "New site". Change the Protocol to SFTP. | ||
- For the Host we are going to want to enter public IPv4 address that can be seen on our instance page. E.g 3.95.161.48 | ||
- Under User enter `ec2-user` and press connect. | ||
- On the left hand side we are going to locate our Birdini Bot folder. | ||
- You should now see something like this. ![alttext](https://i.imgur.com/M2CZuJI.png) | ||
- Highlight and drag over all the files into the ec2-user folder. | ||
- Now we have all our files on the server we just need to install some dependencies via the console and we can get the bot running! | ||
- Head back to your AWS instance web page and selec the "Connect" button in the top right, press connect again. This will take you to the AWS console. | ||
- Type `sudo yum install python3-pip`. You will then be asked if this is ok, type `y` and press Enter. | ||
- We can now install the other dependencies using the following commands. | ||
- pip install discord | ||
- pip install python-dotenv | ||
- pip install langcodes | ||
- pip install python_graphql_client | ||
- Now that all our dependencies are finally installed we are ready to start the bot! We will be using the screen command to do this. | ||
- Type `screen -d -m python3 bot.py`. | ||
- The bot should now be running, appear online in discord, and be ready to send notifications! | ||
- If you want to shutdown the bot for whatever reason you can type `screen -ls` to see a list of detatched instances you have running (this should just be one). | ||
- Now to shut this down you want type `screen -X -S [session you want to kill] quit`. | ||
- The session name shown using `screen -ls` will always be different so bare that in mind. | ||
- Here's what my command looked like `screen -X -S 3945..ip-172-31-93-222 quit` | ||
- `3945..ip-172-31-93-222` was the session name that AWS generated for me. | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
# Birdini bot.py | ||
import os | ||
import discord | ||
import json | ||
import datetime | ||
import requests | ||
|
||
from dotenv import load_dotenv | ||
from pybirdbuddy.birdbuddy.client import BirdBuddy | ||
from discord import app_commands | ||
from discord.ext import commands, tasks | ||
from discord.ext.commands import has_permissions | ||
from datetime import datetime | ||
|
||
# Load enviroment variables | ||
load_dotenv() | ||
TOKEN = os.getenv('DISCORD_TOKEN') | ||
EMAIL = os.getenv('BB_NAME') | ||
PASS = os.getenv('BB_PASS') | ||
|
||
# Open json file | ||
with open('discVars.json') as discVars: | ||
jsonData = json.load(discVars) | ||
|
||
# Login to birdbuddy feeder | ||
bb = BirdBuddy(EMAIL, PASS) | ||
bb.language_code = "en" | ||
|
||
intents = discord.Intents.all() | ||
|
||
# This is the URL used to insert multiple images into embeds. This can be changed but it's greatly appreciated if you leave it :) | ||
embedUrl = "https://ko-fi.com/denchy" | ||
|
||
bot = commands.Bot(command_prefix='!', intents=intents) | ||
|
||
|
||
@bot.event | ||
async def on_ready(): | ||
print(f'{bot.user.name} has connected to Discord!') | ||
bird_sighting_new.start() | ||
try: | ||
synced = await bot.tree.sync() | ||
print(f"Synced {len(synced)} command(s)") | ||
except Exception as e: | ||
print(e) | ||
|
||
# Create buttons to attatch to embed | ||
|
||
|
||
class EmbedButtons(discord.ui.View): | ||
def __init__(self): | ||
super().__init__(timeout=None) | ||
# Always add support me button | ||
supportMeButton = discord.ui.Button( | ||
label="Support Me!", style=discord.ButtonStyle.blurple, url=embedUrl, emoji="❤️") | ||
self.add_item(supportMeButton) | ||
|
||
# Change what channel the public messages will be sent too | ||
|
||
|
||
@bot.tree.command(name="channel", description="Define what channel you want to send public notifications to.") | ||
@has_permissions(administrator=True) | ||
@app_commands.describe(channel="Select a channel you wish to push notifications to. You can also enter the channel ID.") | ||
async def set_message_channel(interaction: discord.Interaction, channel: discord.TextChannel): | ||
print(f"Message channel updated to {channel}") | ||
jsonData['ChannelID'] = int(channel.id) | ||
with open("discVars.json", "w") as fileA: | ||
json.dump(jsonData, fileA) | ||
await interaction.response.send_message(f"{bot.user.name} will now post notifications to {channel}", ephemeral=True) | ||
|
||
# Change what channel the 'muted' messages will be sent too | ||
|
||
|
||
@bot.tree.command(name="muted_channel", description="Define what channel you want to redirect public notifications to when muted.") | ||
@has_permissions(administrator=True) | ||
@app_commands.describe(channel="Select a channel you wish to redirect notifications to when muted. You can also enter the channel ID.") | ||
async def set_message_channel_muted(interaction: discord.Interaction, channel: discord.TextChannel): | ||
print(f"Muted message channel updated to {channel}") | ||
jsonData['mutedChannelID'] = int(channel.id) | ||
with open("discVars.json", "w") as fileA: | ||
json.dump(jsonData, fileA) | ||
await interaction.response.send_message(f"{bot.user.name} will now post notifications to {channel} when muted", ephemeral=True) | ||
|
||
|
||
@bot.tree.command(name='mute', description="Mute the bot, the bot will continue to post notifications to your desired privated channel.") | ||
@has_permissions(administrator=True) | ||
async def mute_bot(interaction: discord.Interaction): | ||
if jsonData['isMuted']: | ||
await interaction.response.send_message(f"{bot.user.name} is already muted!", ephemeral=True) | ||
return | ||
jsonData['isMuted'] = True | ||
with open("discVars.json", "w") as fileA: | ||
json.dump(jsonData, fileA) | ||
await interaction.response.send_message(f"{bot.user.name} is now muted and will no longer post to the public channel!", ephemeral=True) | ||
|
||
|
||
@bot.tree.command(name='unmute', description="Unmute the bot, let everyone see those beautiful birds again!") | ||
@has_permissions(administrator=True) | ||
async def unmute_bot(interaction: discord.Interaction): | ||
if not jsonData['isMuted']: | ||
await interaction.response.send_message(f"{bot.user.name} isn't muted!", ephemeral=True) | ||
return | ||
jsonData['isMuted'] = False | ||
with open("discVars.json", "w") as fileA: | ||
json.dump(jsonData, fileA) | ||
await interaction.response.send_message(f"{bot.user.name} is now unmuted and will continue to post to the public channel!", ephemeral=True) | ||
|
||
|
||
# timer stuff | ||
@tasks.loop(seconds=60.0) | ||
async def bird_sighting_new(): | ||
channelID = jsonData['ChannelID'] | ||
mChannelID = jsonData['mutedChannelID'] | ||
isMuted = jsonData['isMuted'] | ||
# channel id you want the bird auto-notifications to be sent to | ||
channel = bot.get_channel(channelID) | ||
mChannel = bot.get_channel(mChannelID) | ||
if channel is None: | ||
print("A valid channel for public alerts was not selected.") | ||
return | ||
elif mChannel is None and isMuted is True: | ||
print("A valid channel for muted alerts was not selected.") | ||
return | ||
# BB CODE | ||
getPostcards = await bb.new_postcards() | ||
|
||
# return if no new postcards are detected | ||
if len(getPostcards) == 0: | ||
print("No new postcards found.") | ||
return | ||
|
||
# after postcard sighting is confirmed use finishPostcard | ||
getSightings = await bb.sighting_from_postcard(getPostcards[0]) | ||
getReport = getSightings.report | ||
# get report status and set type | ||
|
||
imageUrls = [item['contentUrl'] for item in getSightings.medias] | ||
videoUrls = [item['contentUrl'] for item in getSightings.video_media] | ||
|
||
# Determine if there is a video url and select appropriate emoji for embed | ||
if len(videoUrls) > 0: | ||
videoEmoji = 'Yes' | ||
hasVideo = True | ||
else: | ||
videoEmoji = 'No' | ||
hasVideo = False | ||
|
||
appendToDescription = f"\n🖼️ Images captured: {len(imageUrls)} \n📹 Video captured: {videoEmoji}" | ||
|
||
split_string = str(getReport).split("'") | ||
recognized_phrase = split_string[3] | ||
if recognized_phrase == "mystery" or recognized_phrase == "best_guess": | ||
birdIcon = "" | ||
descriptionText = f"🐦 Total visits: ??{appendToDescription}" | ||
embedTitle = "Unidentifiable bird spotted!" | ||
embedColor = 0xb5b5b6 | ||
else: | ||
|
||
birdName = getReport['sightings'][0]['species']['name'] | ||
birdIcon = getReport['sightings'][0]['species']['iconUrl'] | ||
try: | ||
birdVisitCount = getReport['sightings'][0]['count'] | ||
descriptionText = f"🐦 Total visits: {str(birdVisitCount)}{appendToDescription}" | ||
embedTitle = f"{birdName} spotted!" | ||
embedColor = 0x4dff4d | ||
except: | ||
birdVisitCount = 1 | ||
descriptionText = f"This is your first time being visited by a {birdName}!\n\n🐦 Total Visits: 1{appendToDescription}" | ||
embedTitle = f"{birdName} unlocked!" | ||
embedColor = 0xf1c232 | ||
|
||
# Weird embed merge bug thing idk, allows for multiple images in a single embed. Mobile will only show one however. | ||
embed = discord.Embed(title=embedTitle, | ||
url=embedUrl, | ||
description=descriptionText, | ||
color=embedColor, | ||
timestamp=datetime.now()) | ||
# embed.set_image(url=imageUrls[0]) | ||
image_data = requests.get(imageUrls[0]).content | ||
with open('image.png', 'wb') as handler: | ||
handler.write(image_data) | ||
# ^old method linked expires, new one below for test... download and upload the files to discords server and then embed them | ||
file = discord.File("image.png", filename="image.png") | ||
files = [file] | ||
|
||
embed = discord.Embed(title=embedTitle, | ||
url=embedUrl, | ||
description=descriptionText, | ||
color=embedColor, | ||
timestamp=datetime.now()) | ||
embed.set_image(url="attachment://image.png") | ||
embed.set_footer(text="Birdini by Denchy", icon_url=bot.user.avatar) | ||
|
||
embeds = [embed] | ||
|
||
# Process additional images | ||
if len(imageUrls) > 1: | ||
for i in range(1, min(len(imageUrls), 4)): | ||
image_data = requests.get(imageUrls[i]).content | ||
with open(f'image{i}.png', 'wb') as handler: | ||
handler.write(image_data) | ||
file = discord.File(f"image{i}.png", filename=f"image{i}.png") | ||
files.append(file) | ||
|
||
embed = discord.Embed(url=embedUrl) | ||
embed.set_image(url=f"attachment://image{i}.png") | ||
embeds.append(embed) | ||
|
||
# Send embed with attachments to appropriate channel | ||
if isMuted: | ||
await mChannel.send(files=files, embeds=embeds, view=EmbedButtons()) | ||
else: | ||
await channel.send(files=files, embeds=embeds, view=EmbedButtons()) | ||
|
||
# uncomment this to finish postcards and send them to collections! | ||
# await bb.finish_postcard(getPostcards[0]['id'], getSightings) | ||
|
||
bot.run(TOKEN) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"ChannelID": 0, | ||
"isMuted": false, | ||
"mutedChannelID": 0 | ||
} |