omnibase / social
Social publishing for omnibase: a reel rendered from the site’s own template (ffmpeg), published on each network through glitchr/omnipost, and the account’s feed cached for a wall on the site.
Package info
github.com/glitchr-studio/omnibase-social
Type:symfony-bundle
pkg:composer/omnibase/social
Requires
- php: ^8.2
- doctrine/orm: ^2.15|^3.0
- glitchr/omnibase: 3.0.*-dev
- glitchr/omnipost: 1.x-dev
- omnibase/admin: dev-main
- symfony/config: ^6.0|^7.0|^8.0
- symfony/dependency-injection: ^6.0|^7.0|^8.0
- symfony/form: ^6.0|^7.0|^8.0
- symfony/http-kernel: ^6.0|^7.0|^8.0
- symfony/messenger: ^6.0|^7.0|^8.0
- symfony/process: ^6.0|^7.0|^8.0
- symfony/security-bundle: ^6.0|^7.0|^8.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 16:12:26 UTC
README
Social publishing for omnibase: a reel made from the site's own template - its logo, a band with a name on it, its colours, its font, a clip before and after -, sent to each network with what it says there, and the account's feed kept for a wall on the site. A violinist's site posts her concert's excerpt to Instagram and YouTube from its back office; a shop shows its last posts under its products.
The networks are glitchr/omnipost's
providers (instagram, youtube...): this bundle knows the site - its
template, its posts, its settings -, omnipost knows the networks.
The pipeline
A SocialPost is the film as it was shot (or a picture), a caption, its
hashtags, a title for the networks that want one, and one SocialPostTarget
per network it goes to - which may say it otherwise there (its own caption,
title, hashtags, template, and extras such as Instagram's share_to_feed or
YouTube's privacy). Three Messenger messages carry it:
RenderPostMessage- each network's film made byService\Renderer(ffmpeg) with its template: the target's, else the post's, else the network's, else the site's (theTemplatemarked as the site's). A post that says "as it is" (useTemplateoff) is not encoded again. The film lands in<social.storage>/<post id>/<platform>.mp4, its cover (the frame atcoverSecond) beside it. Target:RENDERED.PublishTargetMessage- the post as that network receives it (SocialPost::toPost()->for(Platform)) checked byOmnipost\Validatoragainst the network'scapabilities()and the provider's own rules (a reel is 9:16 on Instagram): a refusal fails the target with the reasons, nothing sent. Otherwisepublish(): the network's id kept,SENTorPROCESSING. At the post's hour when it has one to come: the message is held back (DelayStamp), the networks' own scheduling is not used.CheckTargetMessage- everysocial.check.delayseconds,status()read again until the network saysPUBLISHED(the permalink kept) orFAILED(its reason kept),social.check.attemptstimes at most.
The handlers are thin: Service\Publisher holds the logic. The back office
publishes only after every enabled network passed the checks; the preview
page shows each network's reel, cover, checks, state, permalink and error,
and a button to send one network again.
Route the three messages to an asynchronous transport - a rendering takes a while, and a network processes a reel for a minute or two:
# config/packages/messenger.yaml framework: messenger: routing: Base\Social\Message\RenderPostMessage: async Base\Social\Message\PublishTargetMessage: async Base\Social\Message\CheckTargetMessage: async
The async transport must take delays (Doctrine, Redis, AMQP do; sync://
runs the checks at once and gives up early).
The template
Template: a name, the site's or a network's, a logo (#[Uploader], a
picture) in one of five places at 10-50 % of the width, a band across the
lower third with a text (the artist's name), its colour and the text's, a
font (TTF/OTF upload; DejaVu Sans otherwise), the background behind a film
that is shown whole, a clip before and after, how a film that is not upright
is fitted (COVER: cropped to fill 9:16; CONTAIN: whole, letterboxed), and
fades in and out.
The renderer builds one explicit -filter_complex (Renderer::command() is
pure and tested): scale and crop (or pad) to 1080 x 1920, overlay of the
scaled logo with 64 px margins, drawbox + drawtext for the band, fade
and afade, the intro and outro brought to the same size, rate and sound and
joined with concat; then H.264 High 4.1, yuv420p, 30 fps, CRF 20, AAC
160k, +faststart - what Instagram, YouTube, Facebook and TikTok all take.
ffmpeg in the image
The renderer runs ffmpeg and ffprobe: they belong in the image of whatever renders - the Messenger worker (and the web image, for the preview's checks of a post not yet rendered):
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg fonts-dejavu-core && rm -rf /var/lib/apt/lists/* # alpine: apk add --no-cache ffmpeg font-dejavu
Without them a rendering fails with a RenderingException saying so, and
the target shows it. social.ffmpeg.binary / social.ffmpeg.ffprobe name
other paths; social.ffmpeg.font the band's fallback font.
The public address of a film
Instagram does not take an upload: it is given an address and comes for the
film itself. Service\MediaUrls::for($target) gives that address -
/social/media/{target}/{token}.mp4 (social_media), absolute -, signed with
the kernel's secret over the target's id and the file's path
(Service\MediaToken): it names that one file, cannot be guessed, and
answers only while the publication is on its way, and social.media_url_ttl
seconds (a day) after it ended. Ranges are answered.
The worker has no request to take the host from: set
framework.router.default_uri to the site's public address, and keep
/social/media/ reachable from the internet (no access restriction,
maintenance or launch date page on it).
Install
composer require omnibase/social:dev-main
// config/bundles.php Omnipost\Bridge\Symfony\OmnipostBundle::class => ['all' => true], Base\Social\SocialBundle::class => ['all' => true],
# config/routes.yaml social_controller: resource: "@SocialBundle/src/Controller/Client" type: attribute prefix: / social_admin: resource: "@SocialBundle/src/Controller/Admin/AccountsController.php" type: attribute prefix: /
# config/packages/omnipost.yaml: the app's id and secret; the tokens are the back office's omnipost: providers: instagram: { factory: instagram, options: { app_id: '%env(default::INSTAGRAM_APP_ID)%', app_secret: '%env(default::INSTAGRAM_APP_SECRET)%' } } youtube: { factory: youtube, options: { client_id: '%env(default::YOUTUBE_CLIENT_ID)%', client_secret: '%env(default::YOUTUBE_CLIENT_SECRET)%', channel_id: '%env(default::YOUTUBE_CHANNEL_ID)%' } }
# config/packages/social.yaml (every key optional) social: providers: [instagram] # the omnipost providers the site uses, in order ffmpeg: { binary: ffmpeg, ffprobe: ffprobe, crf: 20, fps: 30 } storage: '%kernel.project_dir%/var/storage/social' feed: { limit: 24, cache_thumbnails: true } media_url_ttl: 86400 # seconds the film's address answers after the end check: { delay: 30, attempts: 20 }
Then bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate
and bin/console assets:install (the stylesheet lives in public/css/social.css).
The accounts
/admin/social (social_admin_accounts) lists each provider: the account
connected (name, picture, followers), when its token dies, and the buttons -
connect (the network's consent page, back at /connect/social/{provider},
social_oauth_callback, to declare in the app), refresh the token, read the
feed now, disconnect - and YouTube's reading key. The tokens are kept in
omnibase's settings, in the vault, as the back office's API keys are:
api.social.<provider>.token (an Omnipost\Model\Token as JSON),
api.social.<provider>.account, api.social.<provider>.api_key.
Service\Accounts::provider($name) builds the provider with them over its
configured options.
Instagram, for one artist's account: a professional account (creator or business), and a Meta app in development mode with the artist added as an Instagram Tester - no App Review is needed for an account of the app's own.
Two crons:
0 * * * * bin/console social:sync # the walls' posts, hourly 30 4 * * * bin/console social:refresh-tokens # tokens that die within ten days (Instagram's live sixty)
On the site
The host's pages ask three Twig functions:
social_wall(12, 'instagram')- the account's last posts in a grid (@Social/client/_wall.html.twig): the thumbnail is the site's own copy (the networks' addresses expire), a film plays muted on hover from the network's address while it answers, each opens the post on the network. A post can be kept off the wall in the back office.social_embed(url)- any public post: an Instagram post or reel (Instagram's own blockquote embed), a YouTube film (youtube-nocookie). Nothing reaches the network before a click - orsocial_embed(url, true)once the visitor said yes, or asocial:consentevent ondocument.social_connected('instagram')- whether the account is connected.
The back office gets SocialPost (render, publish, retry, preview),
Template and SocialMedia CRUDs, the accounts page, and a dashboard
widget, social_overview: each network's last publication and its state,
when the feed was read, a word when a token dies soon.
Tests
tests/ holds unit tests that need no kernel: the ffmpeg command
(RendererCommandTest), SocialPost::toPost() and its variants, the media
token. vendor/bin/phpunit in a checkout with its dependencies.