From e854a84700eafa955458d096abe2d260fe17be70 Mon Sep 17 00:00:00 2001 From: dhawos Date: Wed, 1 Jul 2026 20:18:47 +0200 Subject: [PATCH] first working version --- config.dev.yaml | 3 +- config.yaml | 5 +- .../bskyfeedctl/templates/config.yaml.j2 | 5 + .../templates/docker-compose.yaml.j2 | 14 +- deploy/terraform/main.tf | 13 ++ feeds/lille.go | 2 +- feeds/lille_test.go | 0 flake.nix | 2 +- indexer/elasticsearch.go | 2 + indexer/firehose.go | 15 +- indexer/test_data.json | 200 +++++++++--------- server/config.go | 2 +- server/controller.go | 42 +++- 13 files changed, 188 insertions(+), 117 deletions(-) create mode 100644 feeds/lille_test.go diff --git a/config.dev.yaml b/config.dev.yaml index 3de51c2..12069f9 100644 --- a/config.dev.yaml +++ b/config.dev.yaml @@ -1,7 +1,8 @@ firehose: host: "relay1.eurosky.network" +feed: ownerDid: "at://did:plc:ryyjyoqbea5zmanqtutkbc4o" - feedDid: "did:web:blueskyfeeds.dhawos.dev" + feedDid: "did:web:bskyfeedctl.dhawos.dev" elasticsearch: host: "localhost" port: "9200" diff --git a/config.yaml b/config.yaml index 3de51c2..3caae2b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,10 @@ firehose: host: "relay1.eurosky.network" +server: + host: localhost +feed: ownerDid: "at://did:plc:ryyjyoqbea5zmanqtutkbc4o" - feedDid: "did:web:blueskyfeeds.dhawos.dev" + feedDid: "did:web:bskyfeedctl.dhawos.dev" elasticsearch: host: "localhost" port: "9200" diff --git a/deploy/ansible/roles/bskyfeedctl/templates/config.yaml.j2 b/deploy/ansible/roles/bskyfeedctl/templates/config.yaml.j2 index 00dff47..7c1ea47 100644 --- a/deploy/ansible/roles/bskyfeedctl/templates/config.yaml.j2 +++ b/deploy/ansible/roles/bskyfeedctl/templates/config.yaml.j2 @@ -1,7 +1,12 @@ firehose: host: {{ firehose_relay }} +feed: ownerDid: {{ ownerDid }} feedDid: {{ feedDid }} +server: + addr: 0.0.0.0 + host: {{ hostname }} + port: 8000 elasticsearch: host: "elasticsearch" port: "9200" diff --git a/deploy/ansible/roles/bskyfeedctl/templates/docker-compose.yaml.j2 b/deploy/ansible/roles/bskyfeedctl/templates/docker-compose.yaml.j2 index 610d331..16c57d1 100644 --- a/deploy/ansible/roles/bskyfeedctl/templates/docker-compose.yaml.j2 +++ b/deploy/ansible/roles/bskyfeedctl/templates/docker-compose.yaml.j2 @@ -3,6 +3,8 @@ services: image: {{ image }}:{{ image_tag }} container_name: indexer command: indexer + labels: + - "traefik.enable=false" networks: - main env_file: @@ -18,12 +20,6 @@ services: command: server volumes: - /app/bskyfeedctl/config/config.yaml:/app/config.yaml:ro - labels: - - "traefik.enable=true" - - "traefik.http.routers.blueskyfeeds.rule=Host(`{{ hostname }}`)" - - "traefik.http.routers.blueskyfeeds.entrypoints=websecure" - - "traefik.http.routers.blueskyfeeds.tls=true" - - "traefik.http.routers.blueskyfeeds.tls.certresolver=myresolver" ports: - "8000:8000" networks: @@ -33,6 +29,12 @@ services: depends_on: elasticsearch: condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.blueskyfeeds.rule=Host(`{{ hostname }}`)" + - "traefik.http.routers.blueskyfeeds.entrypoints=websecure" + - "traefik.http.routers.blueskyfeeds.tls=true" + - "traefik.http.routers.blueskyfeeds.tls.certresolver=myresolver" traefik: image: "traefik:v3.7.5" container_name: "traefik" diff --git a/deploy/terraform/main.tf b/deploy/terraform/main.tf index 8ad5278..27c8e94 100644 --- a/deploy/terraform/main.tf +++ b/deploy/terraform/main.tf @@ -3,6 +3,11 @@ variable "project_id" { description = "The project ID" } +variable "dns_zone" { + type = string + description = "The DNS zone to host your record into" +} + resource "scaleway_instance_ip" "public_ip" { project_id = var.project_id } @@ -52,6 +57,14 @@ resource "scaleway_instance_server" "web" { security_group_id = scaleway_instance_security_group.www.id } +resource "scaleway_domain_record" "www" { + dns_zone = var.dns_zone + name = "bskyfeedctl" + type = "A" + data = scaleway_instance_ip.public_ip.address + ttl = 60 +} + output "instance_ip" { value = scaleway_instance_ip.public_ip.address } diff --git a/feeds/lille.go b/feeds/lille.go index 415c8a1..3ac1448 100644 --- a/feeds/lille.go +++ b/feeds/lille.go @@ -116,7 +116,7 @@ func LilleFeed(es *elasticsearch.TypedClient) *Feed { if err != nil { return FeedSkeletonResponse{}, fmt.Errorf("unmarshalling result from elasticsearch: %w", err) } - postResponse := FeedResponse{Post: post.Cid} + postResponse := FeedResponse{Post: post.AtURI} result = append(result, postResponse) lastScore = float64(*hit.Score_) lastCID = post.Cid diff --git a/feeds/lille_test.go b/feeds/lille_test.go new file mode 100644 index 0000000..e69de29 diff --git a/flake.nix b/flake.nix index 37a7076..38ed94d 100644 --- a/flake.nix +++ b/flake.nix @@ -65,7 +65,7 @@ apply = { type = "app"; program = toString (pkgs.writeShellScript "apply" '' - set -E + set -Ee echo "Applyning infrastructure" cd "deploy/terraform" ${pkgs.opentofu}/bin/tofu apply; diff --git a/indexer/elasticsearch.go b/indexer/elasticsearch.go index a76675c..70cfb22 100644 --- a/indexer/elasticsearch.go +++ b/indexer/elasticsearch.go @@ -20,6 +20,7 @@ type ElasticsearchConfig struct { } type PostDocument struct { + AtURI string `json:"atURI"` Cid string `json:"cid"` Text string `json:"text"` Likes int `json:"likes"` @@ -56,6 +57,7 @@ func InitIndex(ctx context.Context, es *elasticsearch.TypedClient) error { properties := esdsl.NewTypeMapping(). AddProperty("cid", didProperty). AddProperty("text", types.NewTextProperty()). + AddProperty("atURI", types.NewKeywordProperty()). AddProperty("likes", types.NewIntegerNumberProperty()). AddProperty("reposts", types.NewIntegerNumberProperty()). AddProperty("createdAt", types.NewDateProperty()) diff --git a/indexer/firehose.go b/indexer/firehose.go index 4db7057..0593e1c 100644 --- a/indexer/firehose.go +++ b/indexer/firehose.go @@ -29,8 +29,9 @@ type FirehoseConfig struct { } type BlueskyPost struct { - cid string - post *bsky.FeedPost + atURI string + cid string + post *bsky.FeedPost } type BlueSkyChannels struct { @@ -82,9 +83,14 @@ func (fh *FirehoseListener) SubscribeFirehose(ctx context.Context, channels *Blu } switch action := rec.(type) { case *bsky.FeedPost: + // If we are in a thread, we don't index the post + if action.Reply != nil { + return nil + } post := &BlueskyPost{ - cid: cid.String(), - post: action, + atURI: fmt.Sprintf("at://%s/%s", r.RepoDid(), op.Path), + cid: cid.String(), + post: action, } channels.Posts <- post } @@ -115,6 +121,7 @@ func HandlePosts(ctx context.Context, input <-chan *BlueskyPost, es *elasticsear } if slices.Contains(post.post.Langs, "fr") { err := insertPost(ctx, es, &PostDocument{ + AtURI: post.atURI, Cid: post.cid, Text: post.post.Text, Likes: 0, diff --git a/indexer/test_data.json b/indexer/test_data.json index a437d19..3ac3465 100644 --- a/indexer/test_data.json +++ b/indexer/test_data.json @@ -1,102 +1,102 @@ [ - {"cid": "cid:dev001", "text": "Je viens de passer 3 heures à chercher un bug qui venait d'un point-virgule manquant. La vie de dev est magnifique.", "likes": 245, "reposts": 34, "createdAt": "2026-05-15T09:23:11Z"}, - {"cid": "cid:dev002", "text": "Le passage à Go pour nos microservices a divisé notre consommation de RAM par dix. Impressionnant !", "likes": 89, "reposts": 12, "createdAt": "2026-05-02T14:45:00Z"}, - {"cid": "cid:dev003", "text": "Est-ce qu'on peut s'accorder sur le fait que centrer une div en CSS n'est plus une excuse valable pour pleurer en 2026 ?", "likes": 512, "reposts": 88, "createdAt": "2026-05-20T18:12:45Z"}, - {"cid": "cid:dev004", "text": "Le cache est vide, la base de données est clean, mais ça ne marche toujours pas. Il est temps de prendre un café.", "likes": 134, "reposts": 45, "createdAt": "2026-05-10T10:05:32Z"}, - {"cid": "cid:dev005", "text": "Docker m'a simplifié la vie, jusqu'au jour où j'ai dû comprendre comment fonctionnaient les networks internes.", "likes": 76, "reposts": 5, "createdAt": "2026-05-17T16:30:21Z"}, - {"cid": "cid:dev006", "text": "Rien de plus satisfaisant que de voir tous les tests unitaires passer au vert du premier coup.", "likes": 210, "reposts": 15, "createdAt": "2026-05-05T08:15:00Z"}, - {"cid": "cid:dev007", "text": "Je refuse de participer au débat Vim vs Emacs. VS Code avec les bons plugins, c'est la paix dans le monde.", "likes": 340, "reposts": 67, "createdAt": "2026-05-14T20:50:18Z"}, - {"cid": "cid:dev008", "text": "Mon historique Git est une tragédie en trois actes : 'wip', 'wip 2', et 'ça marche enfin pitié'.", "likes": 890, "reposts": 230, "createdAt": "2026-05-22T11:42:09Z"}, - {"cid": "cid:dev009", "text": "Le déploiement en production un vendredi soir ? Même pas peur. (Si, complètement terrifié).", "likes": 445, "reposts": 112, "createdAt": "2026-05-13T23:10:55Z"}, - {"cid": "cid:dev010", "text": "Kubernetes est formidable pour gérer la charge, mais la courbe d'apprentissage ressemble à un mur d'escalade sans prises.", "likes": 156, "reposts": 28, "createdAt": "2026-05-03T15:25:33Z"}, - {"cid": "cid:dev011", "text": "Je lis la documentation de cette API depuis ce matin, et j'en déduis que l'auteur déteste l'humanité.", "likes": 321, "reposts": 90, "createdAt": "2026-05-28T09:55:12Z"}, - {"cid": "cid:dev012", "text": "Un client m'a demandé de 'juste rajouter un bouton'. Si seulement il savait ce que ça implique en backend.", "likes": 278, "reposts": 54, "createdAt": "2026-05-08T13:40:22Z"}, - {"cid": "cid:dev013", "text": "TypeScript m'a sauvé de tellement d'erreurs bêtes que je ne peux plus m'en passer aujourd'hui.", "likes": 198, "reposts": 33, "createdAt": "2026-05-30T10:18:47Z"}, - {"cid": "cid:dev014", "text": "Pourquoi mon code marche-t-il sur ma machine locale mais explose complètement en staging ?", "likes": 402, "reposts": 105, "createdAt": "2026-05-12T17:35:04Z"}, - {"cid": "cid:dev015", "text": "La refactorisation de code legacy, c'est comme de l'archéologie : on ne sait jamais sur quoi on va tomber.", "likes": 167, "reposts": 19, "createdAt": "2026-05-19T08:50:11Z"}, - {"cid": "cid:dev016", "text": "Aujourd'hui, j'ai supprimé 500 lignes de code mort. Meilleure sensation de la semaine.", "likes": 289, "reposts": 41, "createdAt": "2026-05-04T14:20:59Z"}, - {"cid": "cid:dev017", "text": "Le syndrome de l'imposteur frappe toujours au moment où on te demande d'estimer la durée d'une tâche complexe.", "likes": 534, "reposts": 145, "createdAt": "2026-05-01T11:11:11Z"}, - {"cid": "cid:dev018", "text": "Je suis développeur fullstack, ce qui signifie que je suis moyen partout et expert nulle part.", "likes": 612, "reposts": 178, "createdAt": "2026-05-25T19:05:34Z"}, - {"cid": "cid:dev019", "text": "Le mode sombre n'est pas une option d'interface, c'est un mode de vie pour préserver mes yeux de développeur.", "likes": 345, "reposts": 72, "createdAt": "2026-05-09T22:45:10Z"}, - {"cid": "cid:dev020", "text": "Quelqu'un a décidé de changer l'architecture au milieu du sprint. Je vais pleurer en silence.", "likes": 123, "reposts": 24, "createdAt": "2026-05-16T16:15:28Z"}, - {"cid": "cid:dev021", "text": "L'Open Source est magnifique, sauf quand une dépendance critique est maintenue par une seule personne qui a disparu.", "likes": 476, "reposts": 99, "createdAt": "2026-05-27T09:30:45Z"}, - {"cid": "cid:dev022", "text": "Je passe plus de temps à nommer mes variables qu'à écrire la logique de mes fonctions.", "likes": 234, "reposts": 56, "createdAt": "2026-05-14T12:55:00Z"}, - {"cid": "cid:dev023", "text": "Le machine learning, c'est beaucoup de statistiques, énormément de données, et un peu de magie noire.", "likes": 189, "reposts": 37, "createdAt": "2026-05-18T08:25:39Z"}, - {"cid": "cid:dev024", "text": "Quand tu lances ta compilation en Rust et que tu as le temps d'aller préparer un repas complet avant la fin.", "likes": 312, "reposts": 65, "createdAt": "2026-05-05T14:10:15Z"}, - {"cid": "cid:dev025", "text": "La sécurité informatique devrait être pensée dès le premier jour, pas ajoutée comme un pansement à la fin.", "likes": 145, "reposts": 21, "createdAt": "2026-05-30T17:40:50Z"}, - {"cid": "cid:lil026", "text": "Rien de tel qu'une pinte en terrasse sur la Grand'Place de Lille quand le soleil pointe enfin le bout de son nez.", "likes": 412, "reposts": 34, "createdAt": "2026-05-08T18:20:12Z"}, - {"cid": "cid:lil027", "text": "Le marché de Wazemmes le dimanche matin, c'est vraiment l'âme de la ville. Les couleurs, les odeurs, l'ambiance !", "likes": 289, "reposts": 45, "createdAt": "2026-05-12T10:15:45Z"}, - {"cid": "cid:lil028", "text": "Un vrai Welsh au Maroilles, avec une bonne portion de frites. C'est lourd, c'est gras, mais c'est tellement bon.", "likes": 567, "reposts": 112, "createdAt": "2026-05-05T13:30:00Z"}, - {"cid": "cid:lil029", "text": "Se perdre volontairement dans les petites rues pavées du Vieux-Lille reste mon activité préférée du week-end.", "likes": 345, "reposts": 56, "createdAt": "2026-05-22T15:45:33Z"}, - {"cid": "cid:lil030", "text": "Le parc de la Citadelle en automne offre des couleurs incroyables, parfait pour une balade digestive.", "likes": 198, "reposts": 23, "createdAt": "2026-05-15T14:20:10Z"}, - {"cid": "cid:lil031", "text": "Je suis passé par la gare Saint-Sauveur hier soir, l'exposition temporaire est vraiment dingue cette année.", "likes": 156, "reposts": 18, "createdAt": "2026-05-28T21:10:05Z"}, - {"cid": "cid:lil032", "text": "La Braderie me manque. L'odeur des moules, les montagnes de coquilles, chiner des vieux vinyles dès l'aube...", "likes": 890, "reposts": 245, "createdAt": "2026-05-03T07:45:00Z"}, - {"cid": "cid:lil033", "text": "Traverser Euralille aux heures de pointe m'a rappelé pourquoi j'évite les centres commerciaux en semaine.", "likes": 112, "reposts": 14, "createdAt": "2026-05-18T18:05:22Z"}, - {"cid": "cid:lil034", "text": "Le beffroi de l'Hôtel de Ville éclairé dans la brume nocturne, ça donne des photos dignes d'un film noir.", "likes": 423, "reposts": 67, "createdAt": "2026-05-11T23:50:40Z"}, - {"cid": "cid:lil035", "text": "Impossible de passer par la rue de Béthune sans croiser quelqu'un qu'on connaît, c'est la loi de Lille.", "likes": 234, "reposts": 31, "createdAt": "2026-05-19T16:25:15Z"}, - {"cid": "cid:lil036", "text": "Un merveilleux de chez Fred pour le goûter, c'est le genre de petit plaisir qu'on ne refuse jamais.", "likes": 678, "reposts": 145, "createdAt": "2026-05-25T15:40:55Z"}, - {"cid": "cid:lil037", "text": "Le métro lillois en plein été, sans climatisation... une expérience tropicale que je ne recommande pas.", "likes": 345, "reposts": 89, "createdAt": "2026-05-14T17:15:30Z"}, - {"cid": "cid:lil038", "text": "J'ai découvert un petit estaminet planqué vers la rue de la Monnaie, l'accueil était chaleureux comme on les aime ici.", "likes": 187, "reposts": 22, "createdAt": "2026-05-02T20:30:12Z"}, - {"cid": "cid:lil039", "text": "Le stade Pierre Mauroy bouillonnait ce week-end pour le match du LOSC. Une ambiance vraiment spectaculaire !", "likes": 512, "reposts": 98, "createdAt": "2026-05-12T22:15:48Z"}, - {"cid": "cid:lil040", "text": "Trouver une place pour se garer vers République le samedi après-midi relève de la mission impossible.", "likes": 145, "reposts": 17, "createdAt": "2026-05-04T14:55:20Z"}, - {"cid": "cid:lil041", "text": "La vue depuis la terrasse panoramique d'Euratechnologies au coucher du soleil est très sous-estimée.", "likes": 267, "reposts": 39, "createdAt": "2026-05-20T21:45:00Z"}, - {"cid": "cid:lil042", "text": "Lire un livre tranquillement assis sur les marches de l'Opéra pendant que la ville s'agite autour, un vrai bonheur.", "likes": 198, "reposts": 24, "createdAt": "2026-05-10T11:20:35Z"}, - {"cid": "cid:lil043", "text": "La pluie lilloise a son propre caractère : elle ne mouille pas beaucoup mais elle dure trois jours d'affilée.", "likes": 456, "reposts": 77, "createdAt": "2026-05-05T09:10:18Z"}, - {"cid": "cid:lil044", "text": "Le Furet du Nord sur la Grand'Place, je peux y entrer pour cinq minutes et y rester bloqué deux heures sans m'en rendre compte.", "likes": 389, "reposts": 62, "createdAt": "2026-05-28T15:35:42Z"}, - {"cid": "cid:lil045", "text": "Faire du vélo sur les pavés du Vieux-Lille demande une suspension robuste ou des poignets en acier trempé.", "likes": 210, "reposts": 29, "createdAt": "2026-05-05T08:50:11Z"}, - {"cid": "cid:lil046", "text": "La friterie Meunier reste mon incontournable quand je veux manger sur le pouce près de la gare.", "likes": 167, "reposts": 15, "createdAt": "2026-05-16T12:45:30Z"}, - {"cid": "cid:lil047", "text": "La piscine musée de Roubaix, juste à côté, est un chef-d'œuvre absolu. J'y retournerais tous les mois si je pouvais.", "likes": 478, "reposts": 105, "createdAt": "2026-05-25T10:30:25Z"}, - {"cid": "cid:lil048", "text": "Les soirées étudiantes rue Masséna me semblent bien loin maintenant, mais le bruit porte toujours jusqu'à chez moi !", "likes": 123, "reposts": 11, "createdAt": "2026-05-14T23:55:00Z"}, - {"cid": "cid:lil049", "text": "J'ai testé la nouvelle micro-brasserie près de Cormontaigne, leur IPA est une petite merveille d'amertume.", "likes": 189, "reposts": 26, "createdAt": "2026-05-02T19:15:40Z"}, - {"cid": "cid:lil050", "text": "Rien de plus lillois que de terminer une soirée en mangeant une frite-fricadelle à 3h du matin.", "likes": 567, "reposts": 134, "createdAt": "2026-05-15T03:10:05Z"}, - {"cid": "cid:cyc051", "text": "Le vent de face sur 20 kilomètres ce matin m'a fait regretter de ne pas avoir pris l'option vélo électrique.", "likes": 345, "reposts": 45, "createdAt": "2026-05-20T08:40:12Z"}, - {"cid": "cid:cyc052", "text": "Sortie gravel incroyable ce week-end dans la forêt. J'ai de la boue jusqu'aux oreilles mais je suis heureux.", "likes": 210, "reposts": 28, "createdAt": "2026-05-15T16:25:50Z"}, - {"cid": "cid:cyc053", "text": "Crever un pneu sous la pluie à 5 km de la maison, c'est exactement pour ça que j'ai toujours trois chambres à air sur moi.", "likes": 178, "reposts": 19, "createdAt": "2026-05-12T17:10:08Z"}, - {"cid": "cid:cyc054", "text": "Le vélotaf, c'est l'assurance d'arriver au bureau réveillé, de bonne humeur, et avec un bon petit cardio fait pour la journée.", "likes": 678, "reposts": 156, "createdAt": "2026-05-05T07:50:33Z"}, - {"cid": "cid:cyc055", "text": "J'ai enfin réussi à battre mon record personnel sur ce segment Strava en côte. Mes poumons sont en feu.", "likes": 289, "reposts": 37, "createdAt": "2026-05-28T14:15:20Z"}, - {"cid": "cid:cyc056", "text": "Les pistes cyclables qui s'arrêtent brusquement au milieu de nulle part sont la pire aberration urbaine.", "likes": 890, "reposts": 245, "createdAt": "2026-05-18T09:05:45Z"}, - {"cid": "cid:cyc057", "text": "Entendre le doux ronronnement d'une chaîne parfaitement nettoyée et lubrifiée est ASMR pour tout cycliste qui se respecte.", "likes": 412, "reposts": 67, "createdAt": "2026-05-09T18:30:15Z"}, - {"cid": "cid:cyc058", "text": "Les automobilistes qui te frôlent à 80 km/h sur une route de campagne... On partage la route, s'il vous plaît !", "likes": 756, "reposts": 198, "createdAt": "2026-05-02T10:45:00Z"}, - {"cid": "cid:cyc059", "text": "Rien de mieux que le premier coup de pédale un dimanche matin très tôt, quand la ville dort encore.", "likes": 345, "reposts": 52, "createdAt": "2026-05-10T06:15:30Z"}, - {"cid": "cid:cyc060", "text": "Investir dans un bon cuissard est la meilleure décision que j'ai pu prendre. Mon séant me remercie à chaque longue sortie.", "likes": 234, "reposts": 31, "createdAt": "2026-05-20T19:55:10Z"}, - {"cid": "cid:cyc061", "text": "Le bikepacking m'a appris le minimalisme. Chaque gramme compte quand on doit monter un col alpin.", "likes": 198, "reposts": 24, "createdAt": "2026-05-05T13:20:45Z"}, - {"cid": "cid:cyc062", "text": "Je disais que le vélo aérodynamique ne changerait rien, jusqu'à ce que je l'essaye sur le plat. C'est une fusée.", "likes": 156, "reposts": 17, "createdAt": "2026-05-30T15:10:25Z"}, - {"cid": "cid:cyc063", "text": "La règle non écrite du cyclisme : on se salue toujours quand on se croise, peu importe le vélo.", "likes": 567, "reposts": 89, "createdAt": "2026-05-14T09:35:50Z"}, - {"cid": "cid:cyc064", "text": "Faire l'ascension du Mont Ventoux était mon objectif de l'année. La vue au sommet efface toute la douleur.", "likes": 423, "reposts": 76, "createdAt": "2026-05-15T12:00:00Z"}, - {"cid": "cid:cyc065", "text": "Rouler de nuit avec un bon éclairage apporte une ambiance totalement différente, presque méditative.", "likes": 187, "reposts": 22, "createdAt": "2026-05-10T21:40:15Z"}, - {"cid": "cid:cyc066", "text": "Le nettoyage du vélo après une sortie hivernale sur des routes pleines de sel prend plus de temps que la sortie elle-même.", "likes": 210, "reposts": 34, "createdAt": "2026-05-08T16:05:30Z"}, - {"cid": "cid:cyc067", "text": "Un rayon cassé en plein milieu d'une descente, c'est la grosse frayeur garantie. Heureusement, plus de peur que de mal.", "likes": 134, "reposts": 12, "createdAt": "2026-05-22T14:50:45Z"}, - {"cid": "cid:cyc068", "text": "Voir des enfants apprendre à faire du vélo sans roulettes pour la première fois me donne toujours le sourire.", "likes": 456, "reposts": 65, "createdAt": "2026-05-03T11:25:20Z"}, - {"cid": "cid:cyc069", "text": "Le choix des pneus est un débat sans fin. Tubeless ou chambre à air ? La guerre est éternelle sur les forums.", "likes": 278, "reposts": 41, "createdAt": "2026-05-25T18:15:10Z"}, - {"cid": "cid:cyc070", "text": "S'arrêter à la boulangerie à mi-parcours pour manger un flan, c'est la base de l'entraînement d'endurance.", "likes": 612, "reposts": 105, "createdAt": "2026-05-11T10:30:55Z"}, - {"cid": "cid:cyc071", "text": "La sensation de glisse sur un bel asphalte tout neuf, sans aucune imperfection, c'est la perfection absolue.", "likes": 389, "reposts": 54, "createdAt": "2026-05-27T15:45:00Z"}, - {"cid": "cid:cyc072", "text": "Les sas vélos aux feux rouges sont utiles uniquement si les voitures ne viennent pas s'y garer systématiquement.", "likes": 534, "reposts": 123, "createdAt": "2026-05-14T08:20:35Z"}, - {"cid": "cid:cyc073", "text": "Partir pour 30km et finir par en faire 80 parce qu'on s'est perdu et que le paysage était trop beau.", "likes": 467, "reposts": 88, "createdAt": "2026-05-02T16:10:45Z"}, - {"cid": "cid:cyc074", "text": "Les passages à niveau par temps humide : la hantise de tout cycliste qui tient à sa clavicule.", "likes": 321, "reposts": 47, "createdAt": "2026-05-18T09:55:20Z"}, - {"cid": "cid:cyc075", "text": "Réparer soi-même son dérailleur avec un tutoriel sur internet, c'est 20% de technique et 80% de patience.", "likes": 245, "reposts": 33, "createdAt": "2026-05-09T20:05:15Z"}, - {"cid": "cid:cin076", "text": "Le dernier film de Denis Villeneuve prouve encore une fois qu'il est l'un des plus grands réalisateurs de notre époque.", "likes": 789, "reposts": 156, "createdAt": "2026-05-04T22:30:10Z"}, - {"cid": "cid:cin077", "text": "Regarder un film en VF quand il a été pensé pour sa langue d'origine, c'est perdre une grande partie du jeu d'acteur.", "likes": 456, "reposts": 89, "createdAt": "2026-05-21T19:45:55Z"}, - {"cid": "cid:cin078", "text": "Le cinéma d'horreur indépendant propose des œuvres tellement plus intelligentes que les gros blockbusters remplis de jumpscares.", "likes": 345, "reposts": 67, "createdAt": "2026-05-30T23:15:40Z"}, - {"cid": "cid:cin079", "text": "Entendre les gens manger leurs pop-corn la bouche ouverte pendant une scène dramatique silencieuse... L'enfer sur terre.", "likes": 890, "reposts": 234, "createdAt": "2026-05-08T20:20:15Z"}, - {"cid": "cid:cin080", "text": "La photographie de Roger Deakins me laisse sans voix à chaque fois. Chaque plan de ses films est un tableau.", "likes": 512, "reposts": 98, "createdAt": "2026-05-11T14:50:30Z"}, - {"cid": "cid:cin081", "text": "Un bon scénario peut sauver une mauvaise réalisation, mais l'inverse est très rarement vrai.", "likes": 423, "reposts": 76, "createdAt": "2026-05-26T17:35:45Z"}, - {"cid": "cid:cin082", "text": "Je me suis refait l'intégrale du Seigneur des Anneaux en version longue ce week-end. Plus de 11 heures de pur bonheur.", "likes": 956, "reposts": 312, "createdAt": "2026-05-25T11:10:00Z"}, - {"cid": "cid:cin083", "text": "Les effets spéciaux pratiques (maquillage, animatroniques) vieilliront toujours mieux que des CGI bâclés.", "likes": 678, "reposts": 145, "createdAt": "2026-05-16T15:25:20Z"}, - {"cid": "cid:cin084", "text": "Le cinéma coréen ne cesse de me surprendre par sa capacité à mélanger les genres au sein d'un même film.", "likes": 389, "reposts": 65, "createdAt": "2026-05-09T21:05:35Z"}, - {"cid": "cid:cin085", "text": "La musique de Hans Zimmer, c'est littéralement 50% de l'impact émotionnel des films d'Interstellar à Inception.", "likes": 734, "reposts": 167, "createdAt": "2026-05-19T18:40:50Z"}, - {"cid": "cid:cin086", "text": "Aller seul au cinéma un mardi après-midi, c'est un luxe incroyable qu'il faut savourer au moins une fois.", "likes": 412, "reposts": 88, "createdAt": "2026-05-14T14:15:10Z"}, - {"cid": "cid:cin087", "text": "Les fins ouvertes qui te laissent réfléchir pendant des jours après le générique sont de loin mes préférées.", "likes": 289, "reposts": 45, "createdAt": "2026-05-22T09:30:25Z"}, - {"cid": "cid:cin088", "text": "Les remakes en prises de vues réelles des vieux classiques d'animation perdent presque toujours leur âme originelle.", "likes": 567, "reposts": 123, "createdAt": "2026-05-05T16:55:40Z"}, - {"cid": "cid:cin089", "text": "Le générique de fin devrait être obligatoire à regarder, par respect pour les milliers de personnes qui ont fait le film.", "likes": 234, "reposts": 41, "createdAt": "2026-05-28T22:10:15Z"}, - {"cid": "cid:cin090", "text": "Tarantino reste le maître absolu des dialogues qui ne servent à rien pour l'intrigue mais qui définissent parfaitement un personnage.", "likes": 645, "reposts": 112, "createdAt": "2026-05-18T20:45:30Z"}, - {"cid": "cid:cin091", "text": "Je préfère voir un réalisateur prendre des risques et rater son film que regarder une production aseptisée sans saveur.", "likes": 378, "reposts": 72, "createdAt": "2026-05-31T12:20:55Z"}, - {"cid": "cid:cin092", "text": "Les bandes-annonces qui dévoilent littéralement tous les rebondissements de l'intrigue devraient être illégales.", "likes": 823, "reposts": 210, "createdAt": "2026-05-08T10:05:40Z"}, - {"cid": "cid:cin093", "text": "Découvrir une pépite méconnue dans les tréfonds d'une plateforme de streaming est un sentiment merveilleux.", "likes": 198, "reposts": 28, "createdAt": "2026-05-24T23:35:10Z"}, - {"cid": "cid:cin094", "text": "La salle de l'UGC Ciné Cité était pleine à craquer hier soir, ça fait plaisir de voir que les gens vont toujours au cinéma.", "likes": 267, "reposts": 35, "createdAt": "2026-05-18T08:50:25Z"}, - {"cid": "cid:cin095", "text": "Certains acteurs ont un tel charisme qu'ils arrivent à te garder scotché à l'écran même s'ils ne font que boire un café.", "likes": 312, "reposts": 51, "createdAt": "2026-05-14T19:15:30Z"}, - {"cid": "cid:cin096", "text": "Le rythme d'un film est primordial. Un film de 3 heures peut passer en un éclair, et un film d'une heure trente peut sembler interminable.", "likes": 489, "reposts": 84, "createdAt": "2026-05-29T15:40:45Z"}, - {"cid": "cid:cin097", "text": "Je collectionne les affiches de films depuis dix ans, mes murs n'ont plus un seul centimètre carré de libre.", "likes": 145, "reposts": 17, "createdAt": "2026-05-07T11:10:20Z"}, - {"cid": "cid:cin098", "text": "L'ambiance lors des avant-premières avec la présence de l'équipe du film apporte vraiment un plus à l'expérience.", "likes": 223, "reposts": 29, "createdAt": "2026-05-04T20:25:55Z"}, - {"cid": "cid:cin099", "text": "Revoir un film d'enfance avec des yeux d'adulte et découvrir un sous-texte qu'on n'avait jamais compris, c'est fascinant.", "likes": 534, "reposts": 92, "createdAt": "2026-05-30T16:50:10Z"}, - {"cid": "cid:cin100", "text": "Le son Dolby Atmos dans une bonne salle obscure, c'est quand même la garantie d'une immersion totale.", "likes": 367, "reposts": 61, "createdAt": "2026-05-12T22:05:35Z"} + {"atUri":"at://repoDID/app.bsky.feed.post/2","cid": "cid:dev001", "text": "Je viens de passer 3 heures à chercher un bug qui venait d'un point-virgule manquant. La vie de dev est magnifique.", "likes": 245, "reposts": 34, "createdAt": "2026-05-15T09:23:11Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/3","cid": "cid:dev002", "text": "Le passage à Go pour nos microservices a divisé notre consommation de RAM par dix. Impressionnant !", "likes": 89, "reposts": 12, "createdAt": "2026-05-02T14:45:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/4","cid": "cid:dev003", "text": "Est-ce qu'on peut s'accorder sur le fait que centrer une div en CSS n'est plus une excuse valable pour pleurer en 2026 ?", "likes": 512, "reposts": 88, "createdAt": "2026-05-20T18:12:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/5","cid": "cid:dev004", "text": "Le cache est vide, la base de données est clean, mais ça ne marche toujours pas. Il est temps de prendre un café.", "likes": 134, "reposts": 45, "createdAt": "2026-05-10T10:05:32Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/6","cid": "cid:dev005", "text": "Docker m'a simplifié la vie, jusqu'au jour où j'ai dû comprendre comment fonctionnaient les networks internes.", "likes": 76, "reposts": 5, "createdAt": "2026-05-17T16:30:21Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/7","cid": "cid:dev006", "text": "Rien de plus satisfaisant que de voir tous les tests unitaires passer au vert du premier coup.", "likes": 210, "reposts": 15, "createdAt": "2026-05-05T08:15:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/8","cid": "cid:dev007", "text": "Je refuse de participer au débat Vim vs Emacs. VS Code avec les bons plugins, c'est la paix dans le monde.", "likes": 340, "reposts": 67, "createdAt": "2026-05-14T20:50:18Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/9","cid": "cid:dev008", "text": "Mon historique Git est une tragédie en trois actes : 'wip', 'wip 2', et 'ça marche enfin pitié'.", "likes": 890, "reposts": 230, "createdAt": "2026-05-22T11:42:09Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/10","cid": "cid:dev009", "text": "Le déploiement en production un vendredi soir ? Même pas peur. (Si, complètement terrifié).", "likes": 445, "reposts": 112, "createdAt": "2026-05-13T23:10:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/11","cid": "cid:dev010", "text": "Kubernetes est formidable pour gérer la charge, mais la courbe d'apprentissage ressemble à un mur d'escalade sans prises.", "likes": 156, "reposts": 28, "createdAt": "2026-05-03T15:25:33Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/12","cid": "cid:dev011", "text": "Je lis la documentation de cette API depuis ce matin, et j'en déduis que l'auteur déteste l'humanité.", "likes": 321, "reposts": 90, "createdAt": "2026-05-28T09:55:12Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/13","cid": "cid:dev012", "text": "Un client m'a demandé de 'juste rajouter un bouton'. Si seulement il savait ce que ça implique en backend.", "likes": 278, "reposts": 54, "createdAt": "2026-05-08T13:40:22Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/14","cid": "cid:dev013", "text": "TypeScript m'a sauvé de tellement d'erreurs bêtes que je ne peux plus m'en passer aujourd'hui.", "likes": 198, "reposts": 33, "createdAt": "2026-05-30T10:18:47Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/15","cid": "cid:dev014", "text": "Pourquoi mon code marche-t-il sur ma machine locale mais explose complètement en staging ?", "likes": 402, "reposts": 105, "createdAt": "2026-05-12T17:35:04Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/16","cid": "cid:dev015", "text": "La refactorisation de code legacy, c'est comme de l'archéologie : on ne sait jamais sur quoi on va tomber.", "likes": 167, "reposts": 19, "createdAt": "2026-05-19T08:50:11Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/17","cid": "cid:dev016", "text": "Aujourd'hui, j'ai supprimé 500 lignes de code mort. Meilleure sensation de la semaine.", "likes": 289, "reposts": 41, "createdAt": "2026-05-04T14:20:59Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/18","cid": "cid:dev017", "text": "Le syndrome de l'imposteur frappe toujours au moment où on te demande d'estimer la durée d'une tâche complexe.", "likes": 534, "reposts": 145, "createdAt": "2026-05-01T11:11:11Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/19","cid": "cid:dev018", "text": "Je suis développeur fullstack, ce qui signifie que je suis moyen partout et expert nulle part.", "likes": 612, "reposts": 178, "createdAt": "2026-05-25T19:05:34Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/20","cid": "cid:dev019", "text": "Le mode sombre n'est pas une option d'interface, c'est un mode de vie pour préserver mes yeux de développeur.", "likes": 345, "reposts": 72, "createdAt": "2026-05-09T22:45:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/21","cid": "cid:dev020", "text": "Quelqu'un a décidé de changer l'architecture au milieu du sprint. Je vais pleurer en silence.", "likes": 123, "reposts": 24, "createdAt": "2026-05-16T16:15:28Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/22","cid": "cid:dev021", "text": "L'Open Source est magnifique, sauf quand une dépendance critique est maintenue par une seule personne qui a disparu.", "likes": 476, "reposts": 99, "createdAt": "2026-05-27T09:30:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/23","cid": "cid:dev022", "text": "Je passe plus de temps à nommer mes variables qu'à écrire la logique de mes fonctions.", "likes": 234, "reposts": 56, "createdAt": "2026-05-14T12:55:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/24","cid": "cid:dev023", "text": "Le machine learning, c'est beaucoup de statistiques, énormément de données, et un peu de magie noire.", "likes": 189, "reposts": 37, "createdAt": "2026-05-18T08:25:39Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/25","cid": "cid:dev024", "text": "Quand tu lances ta compilation en Rust et que tu as le temps d'aller préparer un repas complet avant la fin.", "likes": 312, "reposts": 65, "createdAt": "2026-05-05T14:10:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/26","cid": "cid:dev025", "text": "La sécurité informatique devrait être pensée dès le premier jour, pas ajoutée comme un pansement à la fin.", "likes": 145, "reposts": 21, "createdAt": "2026-05-30T17:40:50Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/27","cid": "cid:lil026", "text": "Rien de tel qu'une pinte en terrasse sur la Grand'Place de Lille quand le soleil pointe enfin le bout de son nez.", "likes": 412, "reposts": 34, "createdAt": "2026-05-08T18:20:12Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/28","cid": "cid:lil027", "text": "Le marché de Wazemmes le dimanche matin, c'est vraiment l'âme de la ville. Les couleurs, les odeurs, l'ambiance !", "likes": 289, "reposts": 45, "createdAt": "2026-05-12T10:15:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/29","cid": "cid:lil028", "text": "Un vrai Welsh au Maroilles, avec une bonne portion de frites. C'est lourd, c'est gras, mais c'est tellement bon.", "likes": 567, "reposts": 112, "createdAt": "2026-05-05T13:30:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/30","cid": "cid:lil029", "text": "Se perdre volontairement dans les petites rues pavées du Vieux-Lille reste mon activité préférée du week-end.", "likes": 345, "reposts": 56, "createdAt": "2026-05-22T15:45:33Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/31","cid": "cid:lil030", "text": "Le parc de la Citadelle en automne offre des couleurs incroyables, parfait pour une balade digestive.", "likes": 198, "reposts": 23, "createdAt": "2026-05-15T14:20:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/32","cid": "cid:lil031", "text": "Je suis passé par la gare Saint-Sauveur hier soir, l'exposition temporaire est vraiment dingue cette année.", "likes": 156, "reposts": 18, "createdAt": "2026-05-28T21:10:05Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/33","cid": "cid:lil032", "text": "La Braderie me manque. L'odeur des moules, les montagnes de coquilles, chiner des vieux vinyles dès l'aube...", "likes": 890, "reposts": 245, "createdAt": "2026-05-03T07:45:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/34","cid": "cid:lil033", "text": "Traverser Euralille aux heures de pointe m'a rappelé pourquoi j'évite les centres commerciaux en semaine.", "likes": 112, "reposts": 14, "createdAt": "2026-05-18T18:05:22Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/35","cid": "cid:lil034", "text": "Le beffroi de l'Hôtel de Ville éclairé dans la brume nocturne, ça donne des photos dignes d'un film noir.", "likes": 423, "reposts": 67, "createdAt": "2026-05-11T23:50:40Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/36","cid": "cid:lil035", "text": "Impossible de passer par la rue de Béthune sans croiser quelqu'un qu'on connaît, c'est la loi de Lille.", "likes": 234, "reposts": 31, "createdAt": "2026-05-19T16:25:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/37","cid": "cid:lil036", "text": "Un merveilleux de chez Fred pour le goûter, c'est le genre de petit plaisir qu'on ne refuse jamais.", "likes": 678, "reposts": 145, "createdAt": "2026-05-25T15:40:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/38","cid": "cid:lil037", "text": "Le métro lillois en plein été, sans climatisation... une expérience tropicale que je ne recommande pas.", "likes": 345, "reposts": 89, "createdAt": "2026-05-14T17:15:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/39","cid": "cid:lil038", "text": "J'ai découvert un petit estaminet planqué vers la rue de la Monnaie, l'accueil était chaleureux comme on les aime ici.", "likes": 187, "reposts": 22, "createdAt": "2026-05-02T20:30:12Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/40","cid": "cid:lil039", "text": "Le stade Pierre Mauroy bouillonnait ce week-end pour le match du LOSC. Une ambiance vraiment spectaculaire !", "likes": 512, "reposts": 98, "createdAt": "2026-05-12T22:15:48Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/41","cid": "cid:lil040", "text": "Trouver une place pour se garer vers République le samedi après-midi relève de la mission impossible.", "likes": 145, "reposts": 17, "createdAt": "2026-05-04T14:55:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/42","cid": "cid:lil041", "text": "La vue depuis la terrasse panoramique d'Euratechnologies au coucher du soleil est très sous-estimée.", "likes": 267, "reposts": 39, "createdAt": "2026-05-20T21:45:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/43","cid": "cid:lil042", "text": "Lire un livre tranquillement assis sur les marches de l'Opéra pendant que la ville s'agite autour, un vrai bonheur.", "likes": 198, "reposts": 24, "createdAt": "2026-05-10T11:20:35Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/44","cid": "cid:lil043", "text": "La pluie lilloise a son propre caractère : elle ne mouille pas beaucoup mais elle dure trois jours d'affilée.", "likes": 456, "reposts": 77, "createdAt": "2026-05-05T09:10:18Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/45","cid": "cid:lil044", "text": "Le Furet du Nord sur la Grand'Place, je peux y entrer pour cinq minutes et y rester bloqué deux heures sans m'en rendre compte.", "likes": 389, "reposts": 62, "createdAt": "2026-05-28T15:35:42Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/46","cid": "cid:lil045", "text": "Faire du vélo sur les pavés du Vieux-Lille demande une suspension robuste ou des poignets en acier trempé.", "likes": 210, "reposts": 29, "createdAt": "2026-05-05T08:50:11Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/47","cid": "cid:lil046", "text": "La friterie Meunier reste mon incontournable quand je veux manger sur le pouce près de la gare.", "likes": 167, "reposts": 15, "createdAt": "2026-05-16T12:45:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/48","cid": "cid:lil047", "text": "La piscine musée de Roubaix, juste à côté, est un chef-d'œuvre absolu. J'y retournerais tous les mois si je pouvais.", "likes": 478, "reposts": 105, "createdAt": "2026-05-25T10:30:25Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/49","cid": "cid:lil048", "text": "Les soirées étudiantes rue Masséna me semblent bien loin maintenant, mais le bruit porte toujours jusqu'à chez moi !", "likes": 123, "reposts": 11, "createdAt": "2026-05-14T23:55:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/50","cid": "cid:lil049", "text": "J'ai testé la nouvelle micro-brasserie près de Cormontaigne, leur IPA est une petite merveille d'amertume.", "likes": 189, "reposts": 26, "createdAt": "2026-05-02T19:15:40Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/51","cid": "cid:lil050", "text": "Rien de plus lillois que de terminer une soirée en mangeant une frite-fricadelle à 3h du matin.", "likes": 567, "reposts": 134, "createdAt": "2026-05-15T03:10:05Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/52","cid": "cid:cyc051", "text": "Le vent de face sur 20 kilomètres ce matin m'a fait regretter de ne pas avoir pris l'option vélo électrique.", "likes": 345, "reposts": 45, "createdAt": "2026-05-20T08:40:12Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/53","cid": "cid:cyc052", "text": "Sortie gravel incroyable ce week-end dans la forêt. J'ai de la boue jusqu'aux oreilles mais je suis heureux.", "likes": 210, "reposts": 28, "createdAt": "2026-05-15T16:25:50Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/54","cid": "cid:cyc053", "text": "Crever un pneu sous la pluie à 5 km de la maison, c'est exactement pour ça que j'ai toujours trois chambres à air sur moi.", "likes": 178, "reposts": 19, "createdAt": "2026-05-12T17:10:08Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/55","cid": "cid:cyc054", "text": "Le vélotaf, c'est l'assurance d'arriver au bureau réveillé, de bonne humeur, et avec un bon petit cardio fait pour la journée.", "likes": 678, "reposts": 156, "createdAt": "2026-05-05T07:50:33Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/56","cid": "cid:cyc055", "text": "J'ai enfin réussi à battre mon record personnel sur ce segment Strava en côte. Mes poumons sont en feu.", "likes": 289, "reposts": 37, "createdAt": "2026-05-28T14:15:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/57","cid": "cid:cyc056", "text": "Les pistes cyclables qui s'arrêtent brusquement au milieu de nulle part sont la pire aberration urbaine.", "likes": 890, "reposts": 245, "createdAt": "2026-05-18T09:05:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/58","cid": "cid:cyc057", "text": "Entendre le doux ronronnement d'une chaîne parfaitement nettoyée et lubrifiée est ASMR pour tout cycliste qui se respecte.", "likes": 412, "reposts": 67, "createdAt": "2026-05-09T18:30:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/59","cid": "cid:cyc058", "text": "Les automobilistes qui te frôlent à 80 km/h sur une route de campagne... On partage la route, s'il vous plaît !", "likes": 756, "reposts": 198, "createdAt": "2026-05-02T10:45:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/60","cid": "cid:cyc059", "text": "Rien de mieux que le premier coup de pédale un dimanche matin très tôt, quand la ville dort encore.", "likes": 345, "reposts": 52, "createdAt": "2026-05-10T06:15:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/61","cid": "cid:cyc060", "text": "Investir dans un bon cuissard est la meilleure décision que j'ai pu prendre. Mon séant me remercie à chaque longue sortie.", "likes": 234, "reposts": 31, "createdAt": "2026-05-20T19:55:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/62","cid": "cid:cyc061", "text": "Le bikepacking m'a appris le minimalisme. Chaque gramme compte quand on doit monter un col alpin.", "likes": 198, "reposts": 24, "createdAt": "2026-05-05T13:20:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/63","cid": "cid:cyc062", "text": "Je disais que le vélo aérodynamique ne changerait rien, jusqu'à ce que je l'essaye sur le plat. C'est une fusée.", "likes": 156, "reposts": 17, "createdAt": "2026-05-30T15:10:25Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/64","cid": "cid:cyc063", "text": "La règle non écrite du cyclisme : on se salue toujours quand on se croise, peu importe le vélo.", "likes": 567, "reposts": 89, "createdAt": "2026-05-14T09:35:50Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/65","cid": "cid:cyc064", "text": "Faire l'ascension du Mont Ventoux était mon objectif de l'année. La vue au sommet efface toute la douleur.", "likes": 423, "reposts": 76, "createdAt": "2026-05-15T12:00:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/66","cid": "cid:cyc065", "text": "Rouler de nuit avec un bon éclairage apporte une ambiance totalement différente, presque méditative.", "likes": 187, "reposts": 22, "createdAt": "2026-05-10T21:40:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/67","cid": "cid:cyc066", "text": "Le nettoyage du vélo après une sortie hivernale sur des routes pleines de sel prend plus de temps que la sortie elle-même.", "likes": 210, "reposts": 34, "createdAt": "2026-05-08T16:05:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/68","cid": "cid:cyc067", "text": "Un rayon cassé en plein milieu d'une descente, c'est la grosse frayeur garantie. Heureusement, plus de peur que de mal.", "likes": 134, "reposts": 12, "createdAt": "2026-05-22T14:50:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/69","cid": "cid:cyc068", "text": "Voir des enfants apprendre à faire du vélo sans roulettes pour la première fois me donne toujours le sourire.", "likes": 456, "reposts": 65, "createdAt": "2026-05-03T11:25:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/70","cid": "cid:cyc069", "text": "Le choix des pneus est un débat sans fin. Tubeless ou chambre à air ? La guerre est éternelle sur les forums.", "likes": 278, "reposts": 41, "createdAt": "2026-05-25T18:15:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/71","cid": "cid:cyc070", "text": "S'arrêter à la boulangerie à mi-parcours pour manger un flan, c'est la base de l'entraînement d'endurance.", "likes": 612, "reposts": 105, "createdAt": "2026-05-11T10:30:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/72","cid": "cid:cyc071", "text": "La sensation de glisse sur un bel asphalte tout neuf, sans aucune imperfection, c'est la perfection absolue.", "likes": 389, "reposts": 54, "createdAt": "2026-05-27T15:45:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/73","cid": "cid:cyc072", "text": "Les sas vélos aux feux rouges sont utiles uniquement si les voitures ne viennent pas s'y garer systématiquement.", "likes": 534, "reposts": 123, "createdAt": "2026-05-14T08:20:35Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/74","cid": "cid:cyc073", "text": "Partir pour 30km et finir par en faire 80 parce qu'on s'est perdu et que le paysage était trop beau.", "likes": 467, "reposts": 88, "createdAt": "2026-05-02T16:10:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/75","cid": "cid:cyc074", "text": "Les passages à niveau par temps humide : la hantise de tout cycliste qui tient à sa clavicule.", "likes": 321, "reposts": 47, "createdAt": "2026-05-18T09:55:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/76","cid": "cid:cyc075", "text": "Réparer soi-même son dérailleur avec un tutoriel sur internet, c'est 20% de technique et 80% de patience.", "likes": 245, "reposts": 33, "createdAt": "2026-05-09T20:05:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/77","cid": "cid:cin076", "text": "Le dernier film de Denis Villeneuve prouve encore une fois qu'il est l'un des plus grands réalisateurs de notre époque.", "likes": 789, "reposts": 156, "createdAt": "2026-05-04T22:30:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/78","cid": "cid:cin077", "text": "Regarder un film en VF quand il a été pensé pour sa langue d'origine, c'est perdre une grande partie du jeu d'acteur.", "likes": 456, "reposts": 89, "createdAt": "2026-05-21T19:45:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/79","cid": "cid:cin078", "text": "Le cinéma d'horreur indépendant propose des œuvres tellement plus intelligentes que les gros blockbusters remplis de jumpscares.", "likes": 345, "reposts": 67, "createdAt": "2026-05-30T23:15:40Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/80","cid": "cid:cin079", "text": "Entendre les gens manger leurs pop-corn la bouche ouverte pendant une scène dramatique silencieuse... L'enfer sur terre.", "likes": 890, "reposts": 234, "createdAt": "2026-05-08T20:20:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/81","cid": "cid:cin080", "text": "La photographie de Roger Deakins me laisse sans voix à chaque fois. Chaque plan de ses films est un tableau.", "likes": 512, "reposts": 98, "createdAt": "2026-05-11T14:50:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/82","cid": "cid:cin081", "text": "Un bon scénario peut sauver une mauvaise réalisation, mais l'inverse est très rarement vrai.", "likes": 423, "reposts": 76, "createdAt": "2026-05-26T17:35:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/83","cid": "cid:cin082", "text": "Je me suis refait l'intégrale du Seigneur des Anneaux en version longue ce week-end. Plus de 11 heures de pur bonheur.", "likes": 956, "reposts": 312, "createdAt": "2026-05-25T11:10:00Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/84","cid": "cid:cin083", "text": "Les effets spéciaux pratiques (maquillage, animatroniques) vieilliront toujours mieux que des CGI bâclés.", "likes": 678, "reposts": 145, "createdAt": "2026-05-16T15:25:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/85","cid": "cid:cin084", "text": "Le cinéma coréen ne cesse de me surprendre par sa capacité à mélanger les genres au sein d'un même film.", "likes": 389, "reposts": 65, "createdAt": "2026-05-09T21:05:35Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/86","cid": "cid:cin085", "text": "La musique de Hans Zimmer, c'est littéralement 50% de l'impact émotionnel des films d'Interstellar à Inception.", "likes": 734, "reposts": 167, "createdAt": "2026-05-19T18:40:50Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/87","cid": "cid:cin086", "text": "Aller seul au cinéma un mardi après-midi, c'est un luxe incroyable qu'il faut savourer au moins une fois.", "likes": 412, "reposts": 88, "createdAt": "2026-05-14T14:15:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/88","cid": "cid:cin087", "text": "Les fins ouvertes qui te laissent réfléchir pendant des jours après le générique sont de loin mes préférées.", "likes": 289, "reposts": 45, "createdAt": "2026-05-22T09:30:25Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/89","cid": "cid:cin088", "text": "Les remakes en prises de vues réelles des vieux classiques d'animation perdent presque toujours leur âme originelle.", "likes": 567, "reposts": 123, "createdAt": "2026-05-05T16:55:40Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/90","cid": "cid:cin089", "text": "Le générique de fin devrait être obligatoire à regarder, par respect pour les milliers de personnes qui ont fait le film.", "likes": 234, "reposts": 41, "createdAt": "2026-05-28T22:10:15Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/91","cid": "cid:cin090", "text": "Tarantino reste le maître absolu des dialogues qui ne servent à rien pour l'intrigue mais qui définissent parfaitement un personnage.", "likes": 645, "reposts": 112, "createdAt": "2026-05-18T20:45:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/92","cid": "cid:cin091", "text": "Je préfère voir un réalisateur prendre des risques et rater son film que regarder une production aseptisée sans saveur.", "likes": 378, "reposts": 72, "createdAt": "2026-05-31T12:20:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/93","cid": "cid:cin092", "text": "Les bandes-annonces qui dévoilent littéralement tous les rebondissements de l'intrigue devraient être illégales.", "likes": 823, "reposts": 210, "createdAt": "2026-05-08T10:05:40Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/94","cid": "cid:cin093", "text": "Découvrir une pépite méconnue dans les tréfonds d'une plateforme de streaming est un sentiment merveilleux.", "likes": 198, "reposts": 28, "createdAt": "2026-05-24T23:35:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/95","cid": "cid:cin094", "text": "La salle de l'UGC Ciné Cité était pleine à craquer hier soir, ça fait plaisir de voir que les gens vont toujours au cinéma.", "likes": 267, "reposts": 35, "createdAt": "2026-05-18T08:50:25Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/96","cid": "cid:cin095", "text": "Certains acteurs ont un tel charisme qu'ils arrivent à te garder scotché à l'écran même s'ils ne font que boire un café.", "likes": 312, "reposts": 51, "createdAt": "2026-05-14T19:15:30Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/97","cid": "cid:cin096", "text": "Le rythme d'un film est primordial. Un film de 3 heures peut passer en un éclair, et un film d'une heure trente peut sembler interminable.", "likes": 489, "reposts": 84, "createdAt": "2026-05-29T15:40:45Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/98","cid": "cid:cin097", "text": "Je collectionne les affiches de films depuis dix ans, mes murs n'ont plus un seul centimètre carré de libre.", "likes": 145, "reposts": 17, "createdAt": "2026-05-07T11:10:20Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/99","cid": "cid:cin098", "text": "L'ambiance lors des avant-premières avec la présence de l'équipe du film apporte vraiment un plus à l'expérience.", "likes": 223, "reposts": 29, "createdAt": "2026-05-04T20:25:55Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/100","cid": "cid:cin099", "text": "Revoir un film d'enfance avec des yeux d'adulte et découvrir un sous-texte qu'on n'avait jamais compris, c'est fascinant.", "likes": 534, "reposts": 92, "createdAt": "2026-05-30T16:50:10Z"}, + {"atUri":"at://repoDID/app.bsky.feed.post/101","cid": "cid:cin100", "text": "Le son Dolby Atmos dans une bonne salle obscure, c'est quand même la garantie d'une immersion totale.", "likes": 367, "reposts": 61, "createdAt": "2026-05-12T22:05:35Z"} ] diff --git a/server/config.go b/server/config.go index b7aa7bb..2faa5f7 100644 --- a/server/config.go +++ b/server/config.go @@ -3,6 +3,6 @@ package server import "tangled.org/dhawos.dev/bskyfeedctl/config" func init() { - config.Loader.SetDefault("server.host", "localhost") + config.Loader.SetDefault("server.addr", "0.0.0.0") config.Loader.SetDefault("server.port", "8000") } diff --git a/server/controller.go b/server/controller.go index 9b57a74..ebd5bb1 100644 --- a/server/controller.go +++ b/server/controller.go @@ -26,6 +26,7 @@ type healthResponse struct { type ServerConfig struct { Host string + Addr string Port uint } @@ -65,10 +66,11 @@ func (bfg *BlueskyFeedGenerator) Start() error { return } }) + root.HandleFunc("/.well-known/did.json", bfg.wellKnownDIDHandler) root.Handle("/xrpc/", http.StripPrefix("/xrpc", xrpc)) loggedRoot := loggingMiddleware(root) - addr := fmt.Sprintf("%s:%d", bfg.serverConfig.Host, bfg.serverConfig.Port) + addr := fmt.Sprintf("%s:%d", bfg.serverConfig.Addr, bfg.serverConfig.Port) slog.Info("Starting custom feed server", slog.String("addr", addr)) err := http.ListenAndServe(addr, loggedRoot) if err != nil { @@ -77,6 +79,39 @@ func (bfg *BlueskyFeedGenerator) Start() error { return nil } +type wellKnownDIDResponseService struct { + ID string `json:"id"` + Type string `json:"type"` + ServiceEndpoint string `json:"serviceEndpoint"` +} + +type wellKnownDIDResponse struct { + Context []string `json:"@context"` + ID string `json:"id"` + Service []wellKnownDIDResponseService `json:"service"` +} + +func (bfg *BlueskyFeedGenerator) wellKnownDIDHandler(w http.ResponseWriter, req *http.Request) { + result := wellKnownDIDResponse{ + Context: []string{"https://www.w3.org/ns/did/v1"}, + ID: bfg.feedConfig.FeedDID, + Service: []wellKnownDIDResponseService{ + { + ID: "#bsky_fg", + Type: "BskyFeedGenerator", + ServiceEndpoint: fmt.Sprintf("https://%s", bfg.serverConfig.Host), + }, + }, + } + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(result) + if err != nil { + slog.Error("error sending well known response", slog.Any("err", err)) + http.Error(w, "", http.StatusInternalServerError) + return + } +} + func (bfg *BlueskyFeedGenerator) appBskyFeedGetFeedSkeletonHandler(w http.ResponseWriter, req *http.Request) { // Parse request feed := req.URL.Query().Get("feed") @@ -102,8 +137,10 @@ func (bfg *BlueskyFeedGenerator) appBskyFeedGetFeedSkeletonHandler(w http.Respon http.Error(w, "", http.StatusBadRequest) return } + fmt.Println(atURI.Authority(), bfg.feedConfig.OwnerDID.Authority()) + if atURI.Authority() != bfg.feedConfig.OwnerDID.Authority() { - slog.Error("invalid atURI authority", "authority", atURI.Authority().String(), "expectedAuthority", bfg.feedConfig.OwnerDID) + slog.Error("invalid atURI authority", "authority", atURI.Authority().String(), "expectedAuthority", bfg.feedConfig.OwnerDID.Authority().String()) http.Error(w, "", http.StatusBadRequest) return } @@ -129,6 +166,7 @@ func (bfg *BlueskyFeedGenerator) appBskyFeedGetFeedSkeletonHandler(w http.Respon http.Error(w, "unable to generate feed", http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(response) if err != nil { slog.Error("unable to encode response", slog.String("feed", atURI.RecordKey().String()), slog.Any("err", err)) -- 2.51.2