Having posts with useful information that your followers can see, share, and discuss is one of the best ways to boost engagement for your Facebook page. One of the easiest ways to do this is having a post that update relevant information on a regular basis. But doing that requires time commitment on your part... unless you automate it.
This post will guide you through the process of creating your own Facebook "bot" that will post a status update to your Facebook page on a regular basis using Python and GitHub Actions.
Before we begin, make sure you have a Page Access Token and have a GitHub account.
We'll write a script that would create something like this and post it to our Facebook page. (For non-Thais, the post should give an update on the USDTHB exchange rate, non-resident funds flow for stocks and bonds, as well as various changes.)
More specifically, the sript will pull various financial market information, create a photo out of it, and post it to the page at the end of each trading day. Let's get started!
The first thing we need to do is create a template. Go with any software of your choice (for me it was Illustrator) and lay out where you'd like your variable texts to be.
tip
It's helpful to think about text justification and fonts right away so you have a good reference when you place your (variable) text.
I end up with something like this:
The template looks a little bland, doesn't it? We'll put a background photo in. You could, of course, go to Photoshop, edit some photos, save them, and call it a day. But we'll go beyond that. We'll try to change the background photo every month, so some image processing is required.
For now, the steps will include blurring the image, and decreasing the brightness so that only a glimpse of the image could be seen.
1from PIL import Image, ImageEnhance, ImageFilter2
3# open our image4bkg_img = Image.open("resources/bkg.png")5
6# blur and dim7b = bkg_img.convert('L').resize((1, 1)).getpixel((0, 0)) # get average brightness8bkg_img = bkg_img.filter(ImageFilter.GaussianBlur(3))9enhancer = ImageEnhance.Brightness(bkg_img)10bkg = enhancer.enhance(40/b)11
12# add in template13template = Image.open("resources/template.png")14bkg.paste(template, (0, 0), template)
Note that we can't just dim the image by some constant amount. Since this will need to work for any image regardless of its original brightness, we need to first find the average brightness of the image (line 5) and dim it accordingly (lines 7 and 8).
Note also that the template we put in on line 11 is the one without placeholder text.
At this point, our bkg
should look something like this:
The next step is to add the info. We'll assume that we already have acquired (through various means... ahem!) the necessary data from various sources (please refer to my [Thai] post on scraping here).
First let's define the fonts that we'll be using. Note that font sizes can be taken readily from Illustrator.
1from PIL import ImageFont2
3font_thb = ImageFont.truetype(font='resources/DB Helvethaica X Bd v3.2.ttf', size=130, index=0, encoding='')4font_flow = ImageFont.truetype(font='resources/DB Helvethaica X Bd v3.2.ttf', size=106, index=0, encoding='')5font_date = ImageFont.truetype(font='resources/DB Helvethaica X Bd v3.2.ttf', size=36, index=0, encoding='')6font_change = ImageFont.truetype(font='resources/DB Helvethaica X v3.2.ttf', size=48, index=0, encoding='')
For areas where we define change, we want positive changes to be in green, and negative changes in red. Some helper functions would be, well, helpful:
1def get_color(v):2 if v >= 0:3 return "#01b651" #green4 return "#cc1111" #red5
6def draw_change_text(draw_obj, pos, v, font=font_change, formatter='{:+.2f}'):7 draw_obj.text(pos, formatter.format(v), font=font, anchor="ms", fill=get_color(v))
The draw_change_text
function takes in the draw object, position of the text,
value, font, and numerical format.
Note that the anchor
option
is set to "ms"
which means we want the position we specify
to be vertically in the middle of the textbox, and horizontally on the text's baseline.
What's left for us to do now is to add the info we need!
1tmp = bkg.copy()2draw = ImageDraw.Draw(tmp)3
4# place texts5
6draw.text((877, 160), curdate.strftime('%d %b %Y'), font=font_date, anchor="rs", fill="white")7draw.text((280, 320), '{:.3f}'.format(var_thb), font=font_thb, anchor="ms", fill="white")8
9draw_change_text(draw, (565, 320), var_thb_1d)10draw_change_text(draw, (695, 320), var_thb_mtd)11draw_change_text(draw, (825, 320), var_thb_ytd)12
13draw_change_text(draw, (280, 590), var_stock, font=font_flow, formatter='{:+,.0f}')14draw_change_text(draw, (280, 680), var_stock_mtd, formatter='{:+,.0f}')15draw_change_text(draw, (280, 780), var_stock_ytd, formatter='{:+,.0f}')16
17draw_change_text(draw, (680, 590), var_bond, font=font_flow, formatter='{:+,.0f}')18draw_change_text(draw, (680, 680), var_bond_mtd, formatter='{:+,.0f}')19draw_change_text(draw, (680, 780), var_bond_ytd, formatter='{:+,.0f}')
Now tmp
should contain the final image that is ready to be uploaded.
The next step is to convert it to byte array and upload it to Facebook.
First we'll convert the image to byte array using io.BytesIO()
:
1img_byte_arr = io.BytesIO()2tmp.save(img_byte_arr, format='png')3img_byte_arr = img_byte_arr.getvalue()
We also would like to caption the image just in case the image doesn't load or people prefer reading.
1msg = (("{}\n\n"2 "เรทเฉลี่ยค่าเงินบาท (THBREF) {:0.3f} บาท ต่อ 1 ดอลลาร์ สรอ.\n\n"3 "ต่างชาติ{}หุ้นสุทธิ {:,.0f} ล้านบาท {}บอนด์สุทธิ {:,.0f} ล้านบาท")4 .format(curdate.strftime('%d/%m/%Y'),5 var_thb,6 'ขาย' if var_stock < 0 else 'ซื้อ', abs(var_stock),7 'ขาย' if var_bond < 0 else 'ซื้อ', abs(var_bond)))
Uploading to Facebook is fairly simple, given that you have a valid
Facebook Page Access Token.
Since your access token should be kept secret,
you should never put in directly into your code.
A general practice is to save in in an environment variable
(here I call it FACEBOOK_ACCESS_TOKEN
)
and use os.getenv(...)
to retrieve it.
1import facebook2import os3
4album_id = os.getenv('ALBUM_ID')5if not album_id:6 album_id = 'me'7graph = facebook.GraphAPI(access_token=os.getenv('FACEBOOK_ACCESS_TOKEN'), version='3.1')8api_request = graph.put_photo(image=img_byte_arr,9 message=msg, album_path=album_id + "/photos")
I also want to upload all the images to a specific album, so I created a new album and get its album ID from the URL. And that's it. Once you get this code running, you should see your image appear in your album (or your timeline).
Now comes the best part. Instead of having to manually run this script every day, you can get GitHub Actions to do it for you!
First let's create requirements.txt
which would tell GitHub Actions
which packages we need to run this script.
For me, my requirement file looks like this:
1requests==2.24.02facebook-sdk==3.1.03Pillow==8.0.1
Right now your project folder should look something like this:
/project- main.py- requirements.txt- /resources - bkg.png - [fonts]
To take advantage of GitHub Actions, you'll need to first
push this project folder to your GitHub repository.
If you're new to git
and not sure how to do that,
there's plenty of information online.
You might start with GitHub Docs.
Remember the FACEBOOK_ACCESS_TOKEN
?
It's only in the environment variable in your local computer.
Luckily, you can put a secret into GitHub repo.
Go to your repo page, Settings, and on the left menu select Secrets.
Add your access token as a secret.
You can name it whatever, but why don't we stay consistent and call it FACEBOOK_ACCESS_TOKEN
too?
Do the same thing for other secrets you need, like API keys, album ID, etc.
Now that secrets are in place, we're ready to create a new workflow! Go to Actions > New workflow > set up a workflow yourself. Paste this code in. Don't worry, we'll go through it line-by-line.
1name: Daily FX Dashboard2
3# Controls when the action will run.4on:5 # Allows you to run this workflow manually from the Actions tab6 workflow_dispatch:7 schedule:8 - cron: '5 11 * * *' # runs at 18:05 (GMT+7)9
10# A workflow run is made up of one or more jobs that can run sequentially or in parallel11jobs:12 update-fx:13 runs-on: ubuntu-latest14 steps:15 - uses: actions/checkout@v216 - uses: actions/setup-python@v217 with:18 python-version: '3.x'19 - name: Install dependencies20 run: |21 python -m pip install --upgrade pip22 pip install -r requirements.txt23 - name: Execute script24 env:25 BOT_API_KEY: ${{ secrets.BOT_API_KEY }}26 FACEBOOK_ACCESS_TOKEN: ${{ secrets.FACEBOOK_ACCESS_TOKEN }}27 ALBUM_ID: ${{ secrets.ALBUM_ID }}28 run: python main.py
The first line is just the name of the workflow.
Lines 4 through 8 specifies on which conditions the workflow should run.
Here, we say that there are two ways to run the workflow:
manually (by specifying workflow_dispatch
)
and on schedule using cron
.
A workflow can consist of many jobs, but here we just have one, called update-fx
.
The job would run on latest version of Ubuntu, and take the steps specified:
- Checkout the repo
- Setup python
- Upgrade
pip
and install packages specified inrequirements.txt
- Set environment variables from repo secrets
- Run
main.py
Anddddd, that's it! We have set up a script to pull info from different sources, generate an image from it, post it to Facebook, and automate all of this... all using free services! I hope you find this useful :)
Lastly, if you would like to check out the source code, you can find it in this repo. Also, the album of the Daily FX Dashboard, updated at the end of each trading day, can be found here.