{"id":44038665,"url":"https://github.com/apankowski/garcon","last_synced_at":"2026-02-07T20:13:22.066Z","repository":{"id":39917881,"uuid":"360267645","full_name":"apankowski/garcon","owner":"apankowski","description":"Bot re-posting lunch posts from chosen Facebook pages on Slack","archived":false,"fork":false,"pushed_at":"2026-01-28T18:51:55.000Z","size":2213,"stargazers_count":3,"open_issues_count":8,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-29T09:18:23.507Z","etag":null,"topics":["bot","facebook","kotlin","lunch","slack"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apankowski.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-04-21T18:25:24.000Z","updated_at":"2025-09-23T10:39:06.000Z","dependencies_parsed_at":"2025-12-16T21:07:18.975Z","dependency_job_id":null,"html_url":"https://github.com/apankowski/garcon","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/apankowski/garcon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apankowski%2Fgarcon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apankowski%2Fgarcon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apankowski%2Fgarcon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apankowski%2Fgarcon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apankowski","download_url":"https://codeload.github.com/apankowski/garcon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apankowski%2Fgarcon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29207406,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-07T17:44:10.191Z","status":"ssl_error","status_checked_at":"2026-02-07T17:44:07.936Z","response_time":63,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","facebook","kotlin","lunch","slack"],"created_at":"2026-02-07T20:13:21.438Z","updated_at":"2026-02-07T20:13:22.058Z","avatar_url":"https://github.com/apankowski.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"./assets/logo.png\" alt=\"Logo\" style=\"width: 300px\" /\u003e\n\u003ch1\u003eGarçon\u003c/h1\u003e\n\u003ca href=\"https://sonarcloud.io/summary/new_code?id=garcon\"\u003e\u003cimg src=\"https://sonarcloud.io/api/project_badges/measure?project=garcon\u0026metric=ncloc\" alt=\"Lines of Code\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://sonarcloud.io/summary/new_code?id=garcon\"\u003e\u003cimg src=\"https://sonarcloud.io/api/project_badges/measure?project=garcon\u0026metric=coverage\" alt=\"Coverage\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://codescene.io/projects/22033\"\u003e\u003cimg src=\"https://codescene.io/projects/22033/status-badges/code-health\" alt=\"Code Health\" /\u003e\u003c/a\u003e\n\nBot re-posting lunch posts from chosen Facebook pages on Slack.\n\u003cbr /\u003e\n\u003cbr /\u003e\n\u003c/div\u003e\n\n## How does it work?\n\nThe bot doesn't use Facebook's [Graph API](https://developers.facebook.com/docs/graph-api/) to get the posts as the Graph API doesn't allow accessing content of Facebook pages willy-nilly and that is exactly what we want to do. Instead, it scrapes necessary data directly from Facebook pages.\n\nAfter extraction comes classification. To say whether a post is a lunch post or not the bot breaks it into a collection of words and searches for predefined keywords. To handle typos, misspellings, etc. the words are matched against keywords using [Damerau-Levenshtein distance](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance).\n\nEach lunch post is then reposted to Slack using its [API](https://api.slack.com/).\n\nFetched posts along with their classification, repost status, etc. are saved in a database to prevent same lunch offers from being reposted multiple times as well as allow the bot to be restarted without loosing data.\n\nThe whole procedure is repeated in regular intervals.\n\n⚠️ I do not endorse scraping Facebook pages.\n\n## Slash commands\n\nThe following [slash commands](https://api.slack.com/interactivity/slash-commands) are supported:\n\n* `/lunch help` - displays short help message listing supported slash commands\n* `/lunch` or `/lunch check` - manually triggers checking for lunch posts\n* `/lunch log` - displays tail of the synchronization log\n\n## Stack\n\nThe service is written in Kotlin and uses the following stack:\n\n* Kotlin 2\n* Gradle 8 (with build script in Kotlin)\n* Spring Boot 3\n* Jooq for database access\n* PostgreSQL 10+\n* Kotest 5 and MockK for tests\n* ArchUnit 1 for architecture tests\n\n## Building\n\nAlways use the Gradle wrapper (`./gradlew`) to build the project from command line.\n\nUseful commands:\n\n* `./gradlew build` - builds the project\n* `./gradlew clean build` - fully rebuilds the project\n* `./gradlew test` - runs all tests\n* `./gradlew bootJar` - build \u0026 package the service as a fat JAR\n* `./gradlew bootRun` - run the service locally (note: requires [configuration](#environment-variables))\n* `./gradlew generateJooq` - (re)generate Jooq classes\n* `./gradlew databaseUp` - run a local, empty, fully migrated PostgreSQL database (convenient for testing the service locally or running integration tests from IDE)\n* `./gradlew databaseDown` - shut down local PostgreSQL database\n\nDuring a build, a local, fully migrated PostgreSQL database is started and shut down after the build.\n\nThe service listens on HTTP port 8080 by default.\n\n## Configuration\n\n### Environment variables\n\n#### Service\n\n| Name                                | Description                                               | Required | Default/Example                           |\n|-------------------------------------|-----------------------------------------------------------|:--------:|-------------------------------------------|\n| `PORT`                              | HTTP port that will serve requests                        |    ✗     | `8080`                                    |\n| `ACTUATOR_PORT`                     | HTTP port that will serve [Actuator endpoints](#actuator) |    ✗     | `8081`                                    |\n| `JDBC_DATABASE_URL`                 | JDBC URL to the database                                  |    ✗     | `jdbc:postgresql://localhost:5432/garcon` |\n| `JDBC_DATABASE_USERNAME`            | Username used to connect to the database                  |    ✗     | `garcon`                                  |\n| `JDBC_DATABASE_PASSWORD`            | Password used to connect to the database                  |    ✗     | `garcon`                                  |\n| `LOGGING_STRUCTURED_FORMAT_CONSOLE` | Structured logging format                                 |    ✗     | `ecs`, `gelf`, `logstash`; default: off   |\n\n#### Application\n\n| Name                                                                                    | Description                                                                                                                                                                                  | Required | Default/Example                                                                |\n|-----------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|--------------------------------------------------------------------------------|\n| `LUNCH_SYNC_INTERVAL`                                                                   | Interval between consecutive synchronizations of lunch posts.                                                                                                                                |    ✗     | `PT5M`                                                                         |\n| `LUNCH_CLIENT_USER_AGENT`                                                               | User agent by which the client identifies itself when fetching lunch pages.                                                                                                                  |    ✗     | `Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0` |\n| `LUNCH_CLIENT_TIMEOUT`                                                                  | Max time to wait for the lunch page to be fetched (expressed as ISO 8601 time duration).                                                                                                     |    ✗     | `PT10S`                                                                        |\n| `LUNCH_CLIENT_RETRY_COUNT`                                                              | Number of retries in case of failure.                                                                                                                                                        |    ✗     | `2`                                                                            |\n| `LUNCH_CLIENT_RETRY_MIN_JITTER`                                                         | Min wait time between retries.                                                                                                                                                               |    ✗     | `PT0.05S`                                                                      |\n| `LUNCH_CLIENT_RETRY_MAX_JITTER`                                                         | Max wait time between retries.                                                                                                                                                               |    ✗     | `PT3S`                                                                         |\n| `LUNCH_PAGES_\u003cINDEX\u003e_KEY`, e.g. `LUNCH_PAGES_0_KEY`                                     | Textual key of the lunch page, used as fallback for the page name when reposting. Should not change once assigned.                                                                           |    ✓     | `PŻPS`                                                                         |\n| `LUNCH_PAGES_\u003cINDEX\u003e_URL`, e.g. `LUNCH_PAGES_0_URL`                                     | URL of the lunch page.                                                                                                                                                                       |    ✓     | `https://www.facebook.com/1597565460485886/posts/`                             |\n| `LUNCH_POST_LOCALE`                                                                     | Locale of text of posts used while extracting their keywords.                                                                                                                                |    ✗     | `Locale.ENGLISH`                                                               |\n| `LUNCH_POST_KEYWORDS_\u003cINDEX\u003e_TEXT`, e.g. `LUNCH_POST_KEYWORDS_0_TEXT`                   | The keyword that makes a post be considered as a lunch post, e.g. `lunch` or `menu`.                                                                                                         |    ✗     | `lunch`                                                                        |\n| `LUNCH_POST_KEYWORDS_\u003cINDEX\u003e_EDIT_DISTANCE`, e.g. `LUNCH_POST_KEYWORDS_0_EDIT_DISTANCE` | Maximum allowed [Damerau-Levenshtein distance](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance) between any word from a post and the lunch keyword. Typically `1` or `2`. |    ✗     | `1`                                                                            |\n| `LUNCH_SLACK_SIGNING_SECRET`                                                            | Signing secret of the Slack app used for request verification. Request verification is disabled if the property is not set.                                                                  |    ✗     | `******`                                                                       |\n| `LUNCH_SLACK_TOKEN`                                                                     | Token of the Slack app privileged to send and update reposts. Starts with `xoxb-`.                                                                                                           |    ✓     | `xoxb-some-token`                                                              |\n| `LUNCH_SLACK_CHANNEL`                                                                   | Channel ID (`C1234567`) or name (`#random`) to send reposts to.                                                                                                                              |    ✓     | `#random`                                                                      |\n| `LUNCH_REPOST_RETRY_INTERVAL`                                                           | Interval between consecutive attempts to retry failed reposts.                                                                                                                               |    ✗     | `PT10M`                                                                        |\n| `LUNCH_REPOST_RETRY_BASE_DELAY`                                                         | Base delay in the exponential backoff between consecutive retries of a failed repost.                                                                                                        |    ✗     | `PT1M`                                                                         |\n| `LUNCH_REPOST_RETRY_MAX_ATTEMPTS`                                                       | Max retry attempts for a failed repost.                                                                                                                                                      |    ✗     | `10`                                                                           |\n\n## Installation\n\n### Slack application\n\nCreate a Slack app if you don't have one already:\n\n1. Go to [Slack Apps](https://api.slack.com/apps) → _Create New App_.\n2. Pick a name \u0026 workspace to which the app should belong.\n3. Configure additional stuff like description \u0026 icon.\n\nConfigure permissions and _Slash Commands_ for the app:\n\n1. Go to [Slack Apps](https://api.slack.com/apps) → click on the name of your app.\n2. Go to _Slash Commands_ (under _Features_ submenu) → _Create New Command_ → _Command_: `/lunch`, _Request URL_: `{BASE_URI}/commands/lunch` where `{BASE_URI}` is the base URI under which the bot is deployed/handles requests → _Save_.\n3. Go to _OAuth \u0026 Permissions_ (under _Features_ submenu) → _Scopes_ section → _Bot Token Scopes_ subsection → _Add an OAuth Scope_ → select `chat:write` scope → confirm.\n4. Go to _OAuth \u0026 Permissions_ (under _Features_ submenu) → _OAuth Tokens for Your Workspace_ section → Take note of the _Bot User OAuth Token_ (it starts with `xoxb-`). Set bot's `LUNCH_SLACK_TOKEN` [environment variable](#environment-variables) to this value.\n\nInstall the app:\n\n1. Go to [Slack Apps](https://api.slack.com/apps) → click on the name of your app.\n2. Go to _Install App_ (under _Settings_ submenu) → _Install to Workspace_.\n3. In Slack, go to the channel in which lunch notifications are to be received. Type `/app` and select _Add apps to this channel_. Select the Slack application created above.\n\n### PostgreSQL database\n\nCreate an empty PostgreSQL database for the bot with UTF-8 encoding to support emojis 😃. Take note of the credentials and make sure they allow DML \u0026 DDL queries as the service will automatically migrate the database schema.\n\n### Docker image\n\n1. As described in [Building \u0026 Running](#building) section create the fat JAR:\n   ```\n   ./gradlew bootJar\n   ```\n2. Build the docker image:\n   ```\n   docker build -t garcon .\n   ```\n3. Push built image to the docker registry of your choosing\n4. Configure [environment variables](#environment-variables)\n5. Deploy to the target environment\n\n## Management \u0026 observability\n\n### Actuator\n\nSpring Boot [Actuator endpoints](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html) are exposed under `/internal` prefix. By default, Actuator endpoints are available under a different port than the API - see `ACTUATOR_PORT` environment variable.\n\n### Logging\n\nBy default, the service outputs logs to the console in a human-readable format.\n\nTo switch to structured logging, set `LOGGING_STRUCTURED_FORMAT_CONSOLE` environment variable to:\n\n* `ecs` for Elastic Common Schema,\n* `gelf` for Graylog Extended Log Format, or\n* `logstash` for Logstash.\n\n### Metrics\n\n[Prometheus](https://prometheus.io/) scrape endpoint is exposed under `/internal/prometheus`. It provides [many metrics](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.metrics.supported) out of the box.\n\n## Development\n\n### Possible further work\n\n* Slack configuration testing subcommand sending a test message\n* Update/delete reposts based on upstream\n* Custom business \u0026 technical metrics\n* Adding verification of Slack request timestamps to prevent replay attacks\n* Management / backoffice UI\n* [Instagram](https://www.instagram.com/) support\n\n### Checks\n\nThe repository contains definition of [pre-commit](https://pre-commit.com/) hooks in `.pre-commit-config.yaml`. After installation, before each commit, it automatically runs [Gitleaks](https://gitleaks.io/) on all staged changes.\n\nTo run these checks without making a commit:\n\n* on staged files: `pre-commit run`,\n* on all files: `pre-commit run -a`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapankowski%2Fgarcon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapankowski%2Fgarcon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapankowski%2Fgarcon/lists"}