Skip to content

Repository files navigation

feira — a Feira Hidrográfica

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.

Três portas de entrada

                 ┌────────────────────────────┐
  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
  1. 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.
  2. 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.
  3. 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.

Rodar local

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:8631

O 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).

Testes

cd backend && ../.venv/bin/python -m unittest discover -s tests

Backend 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).

Como é feito

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, um cron + curl faz 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.

O anúncio, em RDF

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 sai

Vocabulá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

./deploy.sh                 # produção
DRY_RUN=true ./deploy.sh    # ensaio: sobe tudo, mas nada sai pras redes

Idempotente. 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ê.

Ligando as redes (uma vez só)

Depois do primeiro deploy. Nada aqui é automático porque depende de contas e painéis de terceiros.

WhatsApp

  1. Ponha o número do Whapi (o mesmo do sabiá) dentro do grupo "Feira Hidrográfica". É um número de verdade, membro comum.
  2. python3 tools/setup_whapi.py groups → copie o id do grupo pro .env (FEIRA_WHATSAPP_GROUP=1203…@g.us).
  3. Invente o segredo do webhook e ponha no .env: FEIRA_WHATSAPP_WEBHOOK_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
  4. ./deploy.sh e então python3 tools/setup_whapi.py install — cadastra o webhook preservando os que o canal já tem.
  5. 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.

Instagram — publicar

  1. A conta @feira.pedalhidrografico precisa ser profissional (Business ou Creator).
  2. 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_basic e instagram_business_content_publish (e instagram_business_manage_messages, pras DMs).
  3. .env: FEIRA_IG_ACCESS_TOKEN=IG… — não é o token do sabiá, que é da conta principal. Confira: python3 tools/setup_instagram.py check.
  4. ./deploy.sh. Daí em diante a feira renova o token sozinha, toda semana.

Instagram — DMs pro WhatsApp

  1. .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) e FEIRA_IG_VERIFY_TOKEN (invente um). ./deploy.sh.
  2. No painel, Configure webhooks: Callback URL https://feira.pedalhidrografi.co/api/webhooks/instagram, o mesmo verify token, e assine o campo messages.
  3. 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).
  4. 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.

Decisões que valem a pena saber

  • 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_PASSPHRASE fica 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.

Limites conhecidos

  • 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).

Licença

GPL-3.0 — ver LICENSE. A IBM Plex Mono (lib/fonts/) é da IBM, sob a SIL Open Font License 1.1.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages