O quadro de avisos da comunidade do Pedal Hidrográfico: quem tem anuncia uma oferta, quem precisa anuncia uma procura, e a feira faz as duas pontas se acharem. No ar em https://feira.pedalhidrografi.co.
A feira é casamenteira, não caixa: não vende, não cobra, não intermedeia pagamento e não tem opinião sobre como o valor deve circular. Dinheiro, troca, doação, empréstimo, aluguel, mutirão, um café — cada anúncio diz como topa, num campo livre, e quem combina são as pessoas.
┌────────────────────────────┐
site ─────────▶│ │──▶ post + story no Instagram
(senha) │ feira (Flask + Turtle) │ @feira.pedalhidrografico
│ │──▶ story "morno" 2×/dia
WhatsApp ─────▶│ anuncios.ttl ◀── SHACL │ (anúncio sorteado)
"ANUNCIO …" │ │
│ │──▶ resposta no grupo, com o link
DM no ────────▶│ │──▶ DM repassada pro grupo
Instagram └────────────────────────────┘ do WhatsApp
- O site. A vitrine com todos os anúncios. Anunciar, editar e apagar pedem a senha da comunidade — uma senha só, de boca a boca, comparada sem acento nem caixa. Quem tem a senha mexe em qualquer anúncio, como numa wiki.
- O grupo "Feira Hidrográfica" no WhatsApp. Mensagem que começa com
ANUNCIO(em maiúsculas; pode ser a legenda de uma foto) vira anúncio. O robô adivinha se é oferta ou procura, as modalidades e o valor, e responde no grupo com o link. O telefone de quem mandou não é publicado. - O Instagram. Todo anúncio novo vira post (carrossel: a arte + as fotos) e story. Duas vezes por dia um anúncio ativo é sorteado pra um story que mantém a conta morna — sorteio justo: pesa mais quem está há mais tempo sem aparecer. DM mandada pra conta é repassada pro grupo do WhatsApp, e a pessoa recebe um aviso automático disso.
O WhatsApp e o Instagram são opcionais: sem as credenciais deles a feira funciona sozinha, só com o site.
python3 -m venv .venv && .venv/bin/pip install -r backend/requirements.txt
cp .env.example .env # preencha FEIRA_PASSPHRASE (e FEIRA_JOB_TOKEN)
cd backend && ../.venv/bin/python main.py
# abra http://127.0.0.1:8631O estado fica em local-state/ (fora do git). Por padrão é dry-run: nada
sai pro WhatsApp nem pro Instagram — as chamadas são simuladas e aparecem no
log. Pra ver a fila do Instagram andar de mentira, ponha no .env um
FEIRA_IG_ACCESS_TOKEN=qualquer-coisa e FEIRA_LOCAL_TICK_SECONDS=10; as
artes geradas aparecem em local-state/cards/.
Mudou o .env? Reinicie o backend (a configuração é lida na subida).
cd backend && ../.venv/bin/python -m unittest discover -s testsBackend de verdade sobre um store temporário, com todas as APIs externas
falsas (Whapi e Graph API do Instagram respondem de mentira em
tests/harness.py) — nenhum teste toca a rede nem conta nenhuma. Cobrem a
senha, o CRUD, o portão SHACL, as fotos, o parser das mensagens ANUNCIO, o
webhook do WhatsApp (dedupe, freio de enxurrada), a fila do Instagram
(retomada, nunca-em-dobro, recuo), os stories sorteados, a renovação do token
e o webhook das DMs (assinatura HMAC).
Receita do ecossistema (amora → levabici → datahidro): PWA estática sem build + um Flask só, igual no host local e no Cloud Run, com o estado em RDF/Turtle validado por SHACL — sem banco de dados.
index.html app.js style.css sw.js |
o app (um arquivo de cada; nada de bundler nem CDN) |
data/vocab.ttl |
fonte única dos rótulos, das modalidades e dos radicais do parser |
data/shapes.ttl |
o portão SHACL e os limites de tamanho (o formulário lê daqui) |
backend/main.py |
rotas, senha, webhooks, páginas com Open Graph |
backend/rdfmodel.py |
o mapeamento único dicionário ↔ RDF |
backend/repo.py |
o único caminho até o estado (grafo + state.json), sob lock |
backend/outbox.py |
a fila de saída persistente e o tick |
backend/parser.py |
mensagem ANUNCIO → anúncio (determinístico, sem LLM) |
backend/whatsapp.py instagram.py |
clientes do Whapi.Cloud e do Graph API |
backend/cards.py |
as artes (post 4:5, story 9:16, prévia de link), em Pillow |
backend/storage.py |
filesystem ou bucket GCS (cópia do datahidro/levabici/amora) |
tools/ |
setup_whapi.py, setup_instagram.py, make_icons.py |
cloudflare/ |
o Worker que põe o domínio na frente do Cloud Run |
Duas ideias seguram o resto:
- Criar um anúncio só enfileira. Publicar no Instagram leva dezenas de
segundos e pode falhar; nem o formulário nem o webhook do WhatsApp esperam
por isso. Quem publica é o tick (
POST /api/jobs/tick), que o Cloud Scheduler chama a cada 2 minutos — um job só: a feira decide sozinha o que está na hora (fila, story morno, renovação semanal do token). Num host próprio, umcron+curlfaz o mesmo. - Nunca publicar em dobro. Cada passo externo grava o progresso antes do próximo; se a requisição morrer no meio, a rodada seguinte retoma. Se o Instagram publicou mas a resposta se perdeu, o post é reencontrado pela URL do anúncio na legenda, em vez de publicado de novo.
Detalhes de operação (variáveis, estado, histórico, API, problemas comuns):
backend/README.md.
Reuso antes de cunhar: o schema.org já tem os dois lados da feira.
an:bike-caloi-10-3f9a a fe:Announcement, schema:Offer ; # ou schema:Demand
schema:name "Bike Caloi 10 aro 27" ;
schema:description "Revisada, pneus novos." ;
fe:exchangeMode fe:modo-dinheiro, fe:modo-troca ; # zero ou mais
fe:terms "R$ 800 ou troco por dobrável" ; # texto livre
schema:contactPoint an:bike-caloi-10-3f9a_contato ;
schema:image an:bike-caloi-10-3f9a_foto1 ;
fe:status fe:status-ativo ; fe:source fe:fonte-site ;
prov:generatedAtTime "…"^^xsd:dateTime ;
schema:validThrough "…"^^xsd:dateTime ; # todo anúncio vence
schema:subjectOf <https://www.instagram.com/p/…/> . # o post, quando saiVocabulário https://id.pedalhidrografi.co/feira/terms#, anúncios em
…/feira/anuncio/<slug>. Nós aninhados são IRIs determinísticos
(<anúncio>_contato, <anúncio>_fotoN), nunca blank nodes. Sem
categorias, de propósito: com 10–30 anúncios vivos cabe tudo numa tela.
Dados abertos: /data/anuncios.ttl
(o dump público não leva telefones), /data/vocab.ttl, /data/shapes.ttl.
./deploy.sh # produção
DRY_RUN=true ./deploy.sh # ensaio: sobe tudo, mas nada sai pras redesIdempotente. Cria o bucket (privado e versionado), uma service account própria
e mínima, sobe os segredos do .env pro Secret Manager, faz o deploy no Cloud
Run (--max-instances 1), cria o job feira-tick no Cloud Scheduler e publica
o Worker da Cloudflare. O token do Whapi é o mesmo secret whapi-token do
sabiá, que a feira só lê.
Depois do primeiro deploy. Nada aqui é automático porque depende de contas e painéis de terceiros.
- Ponha o número do Whapi (o mesmo do sabiá) dentro do grupo "Feira Hidrográfica". É um número de verdade, membro comum.
python3 tools/setup_whapi.py groups→ copie o id do grupo pro.env(FEIRA_WHATSAPP_GROUP=1203…@g.us).- Invente o segredo do webhook e ponha no
.env:FEIRA_WHATSAPP_WEBHOOK_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))") ./deploy.she entãopython3 tools/setup_whapi.py install— cadastra o webhook preservando os que o canal já tem.- No grupo:
ANUNCIO teste do robô, pode ignorar. Ele responde com o link (apague o anúncio de teste pelo site).
O Whapi dirige o protocolo não-oficial do WhatsApp: zona cinzenta dos termos de
uso, ok pra volume baixo — o mesmo aviso do sabia/scripts/post_whatsapp.py.
- A conta
@feira.pedalhidrograficoprecisa ser profissional (Business ou Creator). - No app da Meta (pode ser o mesmo do sabiá), em Instagram → API setup with
Instagram login, conecte a conta da feira e gere um token de longa duração
com
instagram_business_basiceinstagram_business_content_publish(einstagram_business_manage_messages, pras DMs). .env:FEIRA_IG_ACCESS_TOKEN=IG…— não é o token do sabiá, que é da conta principal. Confira:python3 tools/setup_instagram.py check../deploy.sh. Daí em diante a feira renova o token sozinha, toda semana.
.env:FEIRA_IG_APP_SECRET(o app secret; se a assinatura não bater, liste também o secret do produto Instagram, separado por vírgula) eFEIRA_IG_VERIFY_TOKEN(invente um)../deploy.sh.- No painel, Configure webhooks: Callback URL
https://feira.pedalhidrografi.co/api/webhooks/instagram, o mesmo verify token, e assine o campo messages. - Ponha o app em modo Live (em desenvolvimento a Meta não entrega webhook) e, no Instagram, permita o acesso às mensagens (Configurações → Mensagens → Ferramentas conectadas).
python3 tools/setup_instagram.py subscribe, e teste mandando uma DM de outra conta.
Avise na bio que as DMs são repassadas ao grupo. A resposta automática já diz isso a quem escreve, mas a bio avisa antes.
- Senha da comunidade é a exceção da feira à regra "sem auth" do
ecossistema: ela publica sozinha num Instagram público e precisa de um
quebra-molas. É uma senha compartilhada, não login. O servidor fecha por
padrão: sem
FEIRA_PASSPHRASEfica só-leitura; sem os tokens, os webhooks e o tick recusam tudo. - Privacidade. Telefone só aparece se a pessoa escrever no formulário (que
avisa: é público). O número de quem anuncia pelo WhatsApp nunca é gravado. O
telefone fica fora do dump RDF, do JSON-LD, das páginas sem JS e da legenda
do Instagram. Toda foto é re-codificada no servidor: nenhum metadado (GPS,
aparelho) sobrevive. Apagou o anúncio, a foto sai do ar na hora (nada de CDN:
as fotos são servidas como
private) e some do histórico do bucket em 30 dias. - A palavra
ANUNCIOé rígida (maiúsculas, no começo): cada anúncio vira post público, então "anuncio que vou viajar" no meio da conversa não pode disparar nada. - Todo anúncio vence (60 dias por padrão; editar renova). É o que tira anúncio velho da vitrine e do sorteio dos stories.
- Editar ou apagar um anúncio não mexe no post do Instagram (apagar pela API exige outro tipo de token; ver o README do sabiá). Apague à mão, se precisar.
- Apagar ou editar a mensagem no WhatsApp não mexe no anúncio — use o link.
- Os IRIs em
id.pedalhidrografi.co/feira/…ainda não dereferenciam (como os do levabici e do datahidro).
GPL-3.0 — ver LICENSE. A IBM Plex Mono (lib/fonts/) é da IBM, sob
a SIL Open Font License 1.1.