diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index 26b06d4..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: CI - -on: - pull_request: - branches: [master] - types: [opened, synchronize, reopened] - workflow_dispatch: - -jobs: - facebook_group: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: facebook-post-action - uses: ./ - with: - page_id: ${{ secrets.FACEBOOK_GROUP_ID }} - access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} - message: | - ${{ github.event.repository.name }} - ${{ github.ref_name }} - - Group test successful - url: https://github.com/ReenigneArcher/facebook-post-action - fail_on_error: false - - facebook_page: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: facebook-post-action - uses: ./ - with: - page_id: ${{ secrets.FACEBOOK_PAGE_ID }} - access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} - message: | - ${{ github.event.repository.name }} - ${{ github.ref_name }} - - Page test successful - url: https://github.com/ReenigneArcher/facebook-post-action diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1873c8d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +--- +name: CI + +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened] + push: + branches: [master] + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + action: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: facebook-post-action + uses: ./ + with: + access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} + fail_on_error: true + message: | + ${{ github.event.repository.name }} - ${{ github.ref_name }} test + page_id: ${{ secrets.FACEBOOK_PAGE_ID }} + url: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }} + + release: + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: + # - pytest + - action + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Release + id: setup-release + uses: LizardByte/setup-release-action@v2024.919.143601 + with: + github_token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Create Release + id: action + uses: LizardByte/create-release-action@v2024.919.143026 + with: + allowUpdates: false + artifacts: '' + body: ${{ steps.setup-release.outputs.release_body }} + generateReleaseNotes: ${{ steps.setup-release.outputs.release_generate_release_notes }} + name: ${{ steps.setup-release.outputs.release_tag }} + prerelease: true + tag: ${{ steps.setup-release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..31a1b74 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-alpine3.18 AS base + +RUN python -m pip install --no-cache-dir --upgrade pip setuptools wheel + +COPY . /app + +WORKDIR /app +RUN python -m pip install --no-cache-dir --upgrade -r requirements.txt + +# github will mount the `GITHUB_WORKSPACE` directory to /github/workspace +# https://docs.github.com/en/actions/creating-actions/dockerfile-support-for-github-actions#workdir + +ENTRYPOINT ["python", "/app/action/main.py"] diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index 59d0409..0000000 --- a/HISTORY.md +++ /dev/null @@ -1,5 +0,0 @@ -# Changelog - -## [1.0.0] - 2022-01-10 -### Other -- Initial Version diff --git a/README.md b/README.md index 582243a..7617014 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -#facebook-post-action +# facebook-post-action GitHub Action for posting to a facebook page or group. ## 🎒 Prep Work 1. Get a facebook permanent access token (explained below) using a facebook account that owns the page where you want to post messages. -2. Find the ID of the page that you want to post messages in (explained below). -3. Find the atom feed URL that contains the posts that you wish to share. +2. Find the ID of the page or group that you want to post messages in (explained below). ## 🖥 Workflow example ```yaml @@ -19,64 +18,71 @@ jobs: steps: - name: facebook-post-action - uses: ReenigneArcher/facebook-post-action@v1 + uses: LizardByte/facebook-post-action@master with: - page_id: ${{ secrets.FACEBOOK_PAGE_ID }} access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} + fail_on_eror: True message: | ${{ github.event.repository.name }} ${{ github.ref }} Released ${{ github.event.release.body }} + page_id: ${{ secrets.FACEBOOK_PAGE_ID }} url: ${{ github.event.release.html_url }} - fail_on_eror: True ``` -## 🤫 Environment Secrets +## 🤫 Inputs -- **PAGE_ID**: The page ID where you want to post - **ACCESS_TOKEN**: The permanent facebook access token +- **FAIL_ON_ERROR**: Fail the workflow on error. + Group posts will fail if the facebook app is not installed to the group; however, the message will be posted, + setting this to False will allow the workflow to be successful. - **MESSAGE**: The content to post +- **PAGE_ID**: The page ID where you want to post - **URL**: The url to embed with the post (optional) -- **FAIL_ON_ERROR**: Fail the workflow on error. - Group posts will fail if the facebook app is not installed to the group; however the message will be posted, - setting this to False will allow the workflow run to be successful. ## 👥 How to get a Facebook permanent access token -Following the instructions laid out in Facebook's [extending page tokens documentation][2] I was able to get a page access token that does not expire. +Following the instructions laid out in Facebook's [extending page tokens documentation][2] I was able to get a page +access token that does not expire. I suggest using the [Graph API Explorer][3] for all of these steps except where otherwise stated. -### 0. Create Facebook App ### +### 1. Create Facebook App -**If you already have an app**, skip to step 1. +**If you already have an app**, skip to the next step. 1. Go to [My Apps][4]. 2. Click "+ Add a New App". 3. Set up a website app. -You don't need to change its permissions or anything. You just need an app that won't go away before you're done with your access token. +You don't need to change its permissions or anything. You just need an app that won't go away before you're done with +your access token. -### 1. Get User Short-Lived Access Token ### +### 2. Get User Short-Lived Access Token 1. Go to the [Graph API Explorer][3]. -2. Select the application you want to get the access token for (in the "Facebook App" drop-down menu, not the "My Apps" menu). -3. Click "Get Token" > "Get User Access Token". -4. In the "Add a Permission" drop-down, search and check "pages_manage_posts", "pages_show_list", and "publish_to_groups". -5. Click "Generate Access Token". -6. Grant access from a Facebook account that has access to manage the target page. Note that if this user loses access the final, never-expiring access token will likely stop working. +2. Select the application you want to get the access token for (in the "Meta App" drop-down menu). +3. In the "Add a Permission" drop-down, search and check "pages_manage_posts", "pages_show_list", + and "publish_to_groups". Publishing to groups requires an approved app. +4. Click "Generate Access Token". +5. Grant access from a Facebook account that has access to manage the target page. Note that if this user loses access, + the final, never-expiring, access token will likely stop working. The token that appears in the "Access Token" field is your short-lived access token. -### 2. Generate Long-Lived Access Token ### +### 3. Generate Long-Lived Access Token Following [these instructions][5] from the Facebook docs, make a GET request to -> https://graph.facebook.com/oauth/access_token?grant_type=fb_exchange_token&client_id=**{app_id}**&client_secret=**{app_secret}**&fb_exchange_token=**{short_lived_token}** +``` +https://graph.facebook.com/oauth/access_token?grant_type=fb_exchange_token&client_id=**{app_id}**&client_secret=**{app_secret}**&fb_exchange_token=**{short_lived_token}** +``` entering in your app's ID and secret and the short-lived token generated in the previous step. -You **cannot use the Graph API Explorer**. For some reason it gets stuck on this request. I think it's because the response isn't JSON, but a query string. Since it's a GET request, you can just go to the URL in your browser. +You **cannot use the Graph API Explorer**. For some reason it gets stuck on this request. +I think it's because the response isn't JSON, but a query string. Since it's a GET request, +you can just go to the URL in your browser. The response should look like this: @@ -84,23 +90,30 @@ The response should look like this: {"access_token":"**ABC123**","token_type":"bearer","expires_in":5183791} ``` -"ABC123" will be your long-lived access token. You can put it into the [Access Token Debugger][7] to verify. Under "Expires" it should have something like "2 months". +"ABC123" will be your long-lived access token. You can put it into the [Access Token Debugger][7] to verify. +Under "Expires" it should have something like "2 months". If it says "Never", you can skip the rest of the steps. -### 3. Get User ID ### +### 4. Get User ID Using the long-lived access token, make a GET request to -> https://graph.facebook.com/me?access_token=**{long_lived_access_token}** +``` +https://graph.facebook.com/me?access_token=**{long_lived_access_token}** +``` The `id` field is your account ID. You'll need it for the next step. -### 4. Get Permanent Page Access Token ### +### 5. Get Permanent Page Access Token Make a GET request to -> https://graph.facebook.com/**{account_id}**/accounts?access_token=**{long_lived_access_token}** +``` +https://graph.facebook.com/**{account_id}**/accounts?access_token=**{long_lived_access_token}** +``` -The JSON response should have a `data` field under which is an array of items the user has access to. Find the item for the page you want the permanent access token from. The `access_token` field should have your permanent access token. Copy it and test it in the [Access Token Debugger][7]. Under "Expires" it should say "Never". +The JSON response should have a `data` field under which is an array of items the user has access to. +Find the item for the page you want the permanent access token from. The `access_token` field should have your +permanent access token. Copy it and test it in the [Access Token Debugger][7]. Under "Expires" it should say "Never". [2]:https://developers.facebook.com/docs/facebook-login/access-tokens#extendingpagetokens [3]:https://developers.facebook.com/tools/explorer diff --git a/action.yml b/action.yml index af5c960..f98bb74 100644 --- a/action.yml +++ b/action.yml @@ -1,7 +1,11 @@ --- -name: facebook-post-action -author: LizardByte -description: GitHub Action for posting to a facebook page or group. +name: "Facebook Post Action" +description: "Post to a facebook page or group." +author: "LizardByte" + +branding: + icon: at-sign + color: blue inputs: page_id: @@ -21,19 +25,5 @@ inputs: required: false runs: - using: "composite" - steps: - - run: python -m pip install -r ${{ github.action_path }}/requirements.txt # Installing dependencies - shell: bash - - run: python ${{ github.action_path }}/facebook_post_action.py - shell: bash - env: - INPUT_PAGE_ID: ${{inputs.page_id}} - INPUT_ACCESS_TOKEN: ${{inputs.access_token}} - INPUT_MESSAGE: ${{inputs.message}} - INPUT_URL: ${{inputs.url}} - INPUT_FAIL_ON_ERROR: ${{inputs.fail_on_error}} - -branding: - icon: at-sign - color: blue + using: "docker" + image: "Dockerfile" diff --git a/action/__init__.py b/action/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/action/main.py b/action/main.py new file mode 100644 index 0000000..276b627 --- /dev/null +++ b/action/main.py @@ -0,0 +1,45 @@ +# standard imports +import os +import sys + +# lib imports +from dotenv import load_dotenv +import requests + +load_dotenv() + +# inputs +ACCESS_TOKEN = os.environ['INPUT_ACCESS_TOKEN'] +MESSAGE = os.environ['INPUT_MESSAGE'] +PAGE_ID = os.environ['INPUT_PAGE_ID'] +URL = os.getenv('INPUT_URL', None) +FAIL_ON_ERROR = os.getenv('INPUT_FAIL_ON_ERROR', 'true') + +# constants +FACEBOOK_API_END = f'https://graph.facebook.com/{PAGE_ID}/feed' + + +def main(): + facebook_api_data = { + 'message': MESSAGE, + 'access_token': ACCESS_TOKEN, + } + if URL: + facebook_api_data['link'] = URL + + r = requests.post(url=FACEBOOK_API_END, json=facebook_api_data) + + result = r.json() + + if 'error' not in result: + print('Post successful') + else: + print('Post error:') + print(result) + if FAIL_ON_ERROR.lower() == 'true': + print('Failing the workflow') + sys.exit(1) + + +if __name__ == '__main__': + main() # pragma: no cover diff --git a/facebook_post_action.py b/facebook_post_action.py deleted file mode 100644 index f80507d..0000000 --- a/facebook_post_action.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import requests -import sys - -from dotenv import load_dotenv -load_dotenv() - -# inputs -ACCESS_TOKEN = os.environ['INPUT_ACCESS_TOKEN'] -MESSAGE = os.environ['INPUT_MESSAGE'] -PAGE_ID = os.environ['INPUT_PAGE_ID'] -URL = os.getenv('INPUT_URL', None) -FAIL_ON_ERROR = os.getenv('INPUT_FAIL_ON_ERROR', 'true') - -FACEBOOK_API_END = 'https://graph.facebook.com/{0}/feed'.format(PAGE_ID) - -if URL: - FACEBOOK_API_DATA = {'message': MESSAGE, - 'link': URL, - 'access_token': ACCESS_TOKEN} -else: - FACEBOOK_API_DATA = {'message': MESSAGE, - 'access_token': ACCESS_TOKEN} - -r = requests.post(url=FACEBOOK_API_END, json=FACEBOOK_API_DATA) - -result = r.json() - -if 'error' not in result: - print('Post successful') -else: - print('Post error') - if FAIL_ON_ERROR.lower() == 'true': - print('Failing the workflow') - sys.exit(1) diff --git a/utils.py b/utils.py deleted file mode 100644 index caff181..0000000 --- a/utils.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from datetime import datetime -from urllib.parse import quote -from html.parser import HTMLParser - -if not sys.version_info < (3,): - unicode = str - basestring = str - - -def u(u_string): - """ - Convert a string to unicode working on both python 2 and 3. - - :param u_string: a string to convert to unicode. - - .. versionadded:: 0.1.5 - """ - if isinstance(u_string, unicode): - return u_string - return u_string.decode('utf-8') - - -def s(s_string): - """ - Convert a byte stream to string working on both python 2 and 3. - - :param s_string: a byte stream to convert to string. - - .. versionadded:: 0.1.5 - """ - if isinstance(s_string, bytes): - return s_string - return s_string.encode('utf-8') - - -def html_unescape(_string): - return HTMLParser().unescape(_string) - - -def escape(_string): - return quote(u(_string).encode(), safe='~') - - -def filter_json_index_by_year(json_index_content): - json_index_filtered = {} - current_year = int(datetime.now().strftime('%Y')) - for pid, data in json_index_content.items(): - post_date = datetime.strptime(data['date'][:-6], '%Y-%m-%dT%H:%M:%S') - post_year = int(post_date.strftime('%Y')) - if post_year >= (current_year - 2): - json_index_filtered[pid] = data - return json_index_filtered