Όταν πρόκειται για το σχεδιασμό του API του συστήματος, οι μηχανικοί λογισμικού συχνά εξετάζουν διαφορετικές επιλογές όπως (ή άλλες υβριδικές προσεγγίσεις) για τον προσδιορισμό της καλύτερης προσαρμογής για μια συγκεκριμένη εργασία ή έργο. REST εναντίον RPC εναντίον GraphQL Σε αυτό το άρθρο, διερευνούμε πώς έχει σχεδιαστεί το API αρχικής γραμμής χρόνου ( ) (x.com/home) και ποιες προσεγγίσεις χρησιμοποιούν για την επίλυση των ακόλουθων προκλήσεων: X Twitter Πώς να ανακτήσετε τη λίστα των tweets Πώς να κάνετε μια ταξινόμηση και σελιδοποίηση Πώς να επιστρέψετε τις ιεραρχικές/συνδεδεμένες οντότητες (tweets, χρήστες, μέσα) Πώς να λάβετε λεπτομέρειες tweet Πώς να κάνετε "like" σε ένα tweet Θα διερευνήσουμε αυτές τις προκλήσεις μόνο σε επίπεδο API, αντιμετωπίζοντας την υλοποίηση του backend ως μαύρο κουτί, καθώς δεν έχουμε πρόσβαση στον ίδιο τον κώδικα υποστήριξης. Η εμφάνιση των ακριβών αιτημάτων και απαντήσεων εδώ μπορεί να είναι δυσκίνητη και δύσκολη, καθώς τα βαθιά ένθετα και επαναλαμβανόμενα αντικείμενα είναι δύσκολο να διαβαστούν. Για να διευκολυνθεί η προβολή της δομής ωφέλιμου φορτίου αιτήματος/απόκρισης, προσπάθησα να "πληκτρολογήσω" το API αρχικής γραμμής χρόνου στο TypeScript. Έτσι, όταν πρόκειται για τα παραδείγματα αιτήματος/απόκρισης, θα χρησιμοποιήσω τους αιτήματος και απόκρισης αντί για πραγματικά αντικείμενα JSON. Επίσης, να θυμάστε ότι οι τύποι είναι απλοποιημένοι και πολλές ιδιότητες παραλείπονται για συντομία. τύπους Μπορείτε να βρείτε όλους τους τύπους σε τύπους/χ.τσ αρχείο ή στο κάτω μέρος αυτού του άρθρου στην ενότητα "Παράρτημα: Όλοι οι τύποι σε ένα μέρος". Λήψη της λίστας των tweets Το τελικό σημείο και η δομή αιτήματος/απόκρισης Η ανάκτηση της λίστας των tweet για το αρχικό χρονοδιάγραμμα ξεκινά με το αίτημα στο ακόλουθο τελικό σημείο: POST POST https://x.com/i/api/graphql/{query-id}/HomeTimeline Ακολουθεί ένας απλοποιημένος τύπος σώματος : αιτήματος type TimelineRequest = { queryId: string; // 's6ERr1UxkxxBx4YundNsXw' variables: { count: number; // 20 cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA' seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530659'] }; features: Features; }; type Features = { articles_preview_enabled: boolean; view_counts_everywhere_api_enabled: boolean; // ... } Και εδώ είναι ένας απλοποιημένος τύπος σώματος (θα βουτήξουμε βαθύτερα στους επιμέρους τύπους απόκρισης παρακάτω): απόκρισης type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; }; }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineItem = { entryId: string; // 'tweet-1867041249938530657' sortIndex: string; // '1866561576636152411' content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] }; }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; }; }; type TimelineCursor = { entryId: string; // 'cursor-top-1867041249938530657' sortIndex: string; // '1866961576813152212' content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' cursorType: 'Top' | 'Bottom'; }; }; type ActionKey = string; Είναι ενδιαφέρον να σημειωθεί εδώ, ότι η "λήψη" των δεδομένων γίνεται μέσω "POSTing", το οποίο δεν είναι συνηθισμένο για το API τύπου REST, αλλά είναι σύνηθες για ένα API τύπου GraphQL. Επίσης, το τμήμα της διεύθυνσης URL υποδεικνύει ότι ο X χρησιμοποιεί τη γεύση GraphQL για το API του. graphql Χρησιμοποιώ τη λέξη εδώ επειδή το ίδιο το σώμα της αίτησης δεν μοιάζει με καθαρό , όπου μπορούμε να περιγράψουμε την απαιτούμενη δομή απόκρισης, παραθέτοντας όλες τις ιδιότητες που θέλουμε να ανακτήσουμε: "γεύση" Ερώτημα GraphQL # An example of a pure GraphQL request structure that is *not* being used in the X API. { tweets { id description created_at medias { kind url # ... } author { id name # ... } # ... } } Η υπόθεση εδώ είναι ότι το homeline API δεν είναι ένα καθαρό GraphQL API, αλλά είναι ένας . Η μετάδοση των παραμέτρων σε ένα αίτημα POST όπως αυτό φαίνεται πιο κοντά στη "λειτουργική" κλήση RPC. Αλλά ταυτόχρονα, φαίνεται ότι οι δυνατότητες του GraphQL μπορεί να χρησιμοποιούνται κάπου στο backend πίσω από τον χειριστή/ελεγκτή τελικού σημείου . Ένας συνδυασμός όπως αυτός μπορεί επίσης να προκαλείται από έναν κωδικό παλαιού τύπου ή από κάποιο είδος συνεχιζόμενης μετεγκατάστασης. Αλλά και πάλι, αυτές είναι μόνο οι εικασίες μου. συνδυασμός πολλών προσεγγίσεων HomeTimeline Μπορεί επίσης να παρατηρήσετε ότι το ίδιο χρησιμοποιείται στη διεύθυνση URL του API καθώς και στο σώμα αιτήματος API. Αυτό το queryId πιθανότατα δημιουργείται στο backend, μετά ενσωματώνεται στο bundle και, στη συνέχεια, χρησιμοποιείται κατά την ανάκτηση των δεδομένων από το backend. Μου είναι δύσκολο να καταλάβω πώς ακριβώς χρησιμοποιείται αυτό , καθώς το backend του X είναι ένα μαύρο κουτί στην περίπτωσή μας. Αλλά, και πάλι, η εικασία εδώ μπορεί να είναι ότι, μπορεί να απαιτείται για κάποιο είδος βελτιστοποίησης απόδοσης (επαναχρησιμοποίηση ορισμένων προ-υπολογισμένων αποτελεσμάτων ερωτημάτων;), προσωρινής αποθήκευσης (σχετικά με το Apollo;), εντοπισμού σφαλμάτων (συμμετοχή σε αρχεία καταγραφής μέσω queryId;), ή σκοπούς παρακολούθησης/ιχνηλασίας. TimelineRequest.queryId main.js queryId Είναι επίσης ενδιαφέρον να σημειωθεί ότι το δεν περιέχει μια λίστα με tweets, αλλά μάλλον μια λίστα , όπως (δείτε τον τύπο ) ή (δείτε το τύπος). TimelineResponse οδηγιών "προσθήκη tweet στη γραμμή χρόνου" TimelineAddEntries "τερματισμός της γραμμής χρόνου" TimelineTerminateTimeline Η ίδια η εντολή μπορεί επίσης να περιέχει διαφορετικούς τύπους οντοτήτων: TimelineAddEntries Tweets — δείτε τον τύπο TimelineItem Δρομείς — δείτε τον τύπο TimelineCursor Συζητήσεις/σχόλια/νήματα — δείτε τον τύπο TimelineModule type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here }; Αυτό είναι ενδιαφέρον από την άποψη της επεκτασιμότητας, καθώς επιτρέπει μια ευρύτερη ποικιλία του τι μπορεί να αποδοθεί στο αρχικό χρονοδιάγραμμα χωρίς να τροποποιήσετε υπερβολικά το API. Σελιδοποίηση Η ιδιότητα ορίζει πόσα tweets θέλουμε να ανακτήσουμε ταυτόχρονα (ανά σελίδα). Η προεπιλογή είναι 20. Ωστόσο, περισσότερα από 20 tweets μπορούν να επιστραφούν στον πίνακα . Για παράδειγμα, ο πίνακας μπορεί να περιέχει 37 καταχωρήσεις για τη φόρτωση της πρώτης σελίδας, επειδή περιλαμβάνει tweets (29), καρφιτσωμένα tweets (1), προωθημένα tweets (5) και δρομείς σελιδοποίησης (2). Δεν είμαι σίγουρος γιατί υπάρχουν 29 κανονικά tweets με το ζητούμενο πλήθος των 20. TimelineRequest.variables.count TimelineAddEntries.entries Ο είναι υπεύθυνος για τη σελιδοποίηση που βασίζεται στον δρομέα. TimelineRequest.variables.cursor " χρησιμοποιείται συχνότερα για δεδομένα σε πραγματικό χρόνο λόγω της συχνότητας που προστίθενται νέες εγγραφές και επειδή κατά την ανάγνωση των δεδομένων συχνά βλέπετε πρώτα τα πιο πρόσφατα αποτελέσματα. Εξαλείφει την πιθανότητα παράβλεψης στοιχείων και εμφάνισης του ίδιου στοιχείου περισσότερες από μία φορές. σελιδοποίηση με βάση τον δρομέα, ένας σταθερός δείκτης (ή δρομέας) χρησιμοποιείται για να παρακολουθείτε από πού στο σύνολο δεδομένων πρέπει να ληφθούν τα επόμενα στοιχεία." Δείτε το Η σελιδοποίηση δρομέα Σελιδοποίηση όφσετ έναντι σελιδοποίησης δρομέα νήμα για το πλαίσιο. Κατά τη λήψη της λίστας των tweet για πρώτη φορά, ο είναι κενός, καθώς θέλουμε να φέρουμε τα κορυφαία tweet από την προεπιλεγμένη (πιθανότατα προυπολογισμένη) λίστα εξατομικευμένων tweet. TimelineRequest.variables.cursor Ωστόσο, στην απόκριση, μαζί με τα δεδομένα του tweet, το backend επιστρέφει επίσης τις καταχωρήσεις του δρομέα. Ακολουθεί η ιεραρχία τύπου απάντησης: : TimelineResponse → TimelineAddEntries → TimelineCursor type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here (tweets + cursors) }; type TimelineCursor = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' <-- Here cursorType: 'Top' | 'Bottom'; }; }; Κάθε σελίδα περιέχει τη λίστα των tweets μαζί με τους δρομείς "πάνω" και "κάτω": Αφού φορτωθούν τα δεδομένα της σελίδας, μπορούμε να πάμε από την τρέχουσα σελίδα και προς τις δύο κατευθύνσεις και να φέρουμε είτε τα "προηγούμενα/παλαιότερα" tweet χρησιμοποιώντας τον "κάτω" κέρσορα ή τα "επόμενα/νεότερα" tweets χρησιμοποιώντας τον κέρσορα "επάνω". Η υπόθεσή μου είναι ότι η ανάκτηση των "επόμενων" tweet χρησιμοποιώντας τον κέρσορα "επάνω" συμβαίνει σε δύο περιπτώσεις: όταν τα νέα tweets προστέθηκαν ενώ ο χρήστης εξακολουθεί να διαβάζει την τρέχουσα σελίδα ή όταν ο χρήστης αρχίζει να κάνει κύλιση στη ροή προς τα πάνω (και υπάρχουν δεν υπάρχουν καταχωρήσεις προσωρινής αποθήκευσης ή εάν οι προηγούμενες καταχωρήσεις διαγράφηκαν για λόγους απόδοσης). Ο ίδιος ο κέρσορας του X μπορεί να μοιάζει με αυτό: . Σε ορισμένα σχέδια API, ο δρομέας μπορεί να είναι μια κωδικοποιημένη συμβολοσειρά Base64 που περιέχει το αναγνωριστικό της τελευταίας καταχώρισης στη λίστα ή τη χρονική σήμανση της τελευταίας καταχώρισης που εμφανίστηκε. Για παράδειγμα: και, στη συνέχεια, αυτά τα δεδομένα χρησιμοποιούνται για την ανάλογη αναζήτηση στη βάση δεδομένων. Στην περίπτωση του X API, φαίνεται ότι ο κέρσορας αποκωδικοποιείται από το Base64 σε κάποια προσαρμοσμένη δυαδική ακολουθία που μπορεί να απαιτήσει κάποια περαιτέρω αποκωδικοποίηση για να εξαχθεί οποιοδήποτε νόημα από αυτό (δηλαδή μέσω των ορισμών του μηνύματος Protobuf). Δεδομένου ότι δεν γνωρίζουμε αν είναι κωδικοποίηση και επίσης δεν γνωρίζουμε τον ορισμό του μηνύματος μπορούμε απλώς να υποθέσουμε ότι το backend ξέρει πώς να υποβάλει ερώτημα για την επόμενη παρτίδα tweet με βάση τη συμβολοσειρά του δρομέα. DAABCgABGemI6Mk__9sKAAIZ6MSYG9fQGwgAAwAAAAIAAA eyJpZCI6ICIxMjM0NTY3ODkwIn0= --> {"id": "1234567890"} .proto .proto Η παράμετρος χρησιμοποιείται για να ενημερώσει τον διακομιστή σχετικά με ποια tweets από την τρέχουσα ενεργή σελίδα της άπειρης κύλισης έχει ήδη δει ο πελάτης. Αυτό πιθανότατα βοηθά να διασφαλιστεί ότι ο διακομιστής δεν περιλαμβάνει διπλότυπα tweets στις επόμενες σελίδες αποτελεσμάτων. TimelineResponse.variables.seenTweetIds Συνδεδεμένες/ιεραρχικές οντότητες Μία από τις προκλήσεις που πρέπει να επιλυθούν στα API, όπως το αρχικό χρονοδιάγραμμα (ή το Home Feed) είναι να καταλάβετε πώς να επιστρέψετε τις συνδεδεμένες ή ιεραρχικές οντότητες (π.χ. , , , κ.λπ.): tweet → user tweet → media media → author Θα πρέπει πρώτα να επιστρέψουμε μόνο τη λίστα των tweet και στη συνέχεια να ανακτήσουμε τις εξαρτημένες οντότητες (όπως στοιχεία χρήστη) σε μια δέσμη ξεχωριστών ερωτημάτων κατ' απαίτηση; Ή πρέπει να επιστρέψουμε όλα τα δεδομένα ταυτόχρονα, αυξάνοντας τον χρόνο και το μέγεθος της πρώτης φόρτωσης, αλλά εξοικονομώντας χρόνο για όλες τις επόμενες κλήσεις; Χρειάζεται να κανονικοποιήσουμε τα δεδομένα σε αυτήν την περίπτωση για να μειώσουμε το μέγεθος του ωφέλιμου φορτίου (δηλαδή όταν ο ίδιος χρήστης είναι συγγραφέας πολλών tweet και θέλουμε να αποφύγουμε την επανάληψη των δεδομένων χρήστη ξανά και ξανά σε κάθε οντότητα tweet); Ή θα πρέπει να είναι ένας συνδυασμός των παραπάνω προσεγγίσεων; Ας δούμε πώς το χειρίζεται ο Χ. Νωρίτερα στον τύπο χρησιμοποιήθηκε ο δευτερεύων τύπος . Ας δούμε πώς φαίνεται: TimelineTweet Tweet export type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; // <-- Here // ... }; }; }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; // <-- Here }; type TimelineItem = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; // <-- Here // ... }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; // <-- Here }; }; // A Tweet entity type Tweet = { __typename: 'Tweet'; core: { user_results: { result: User; // <-- Here (a dependent User entity) }; }; legacy: { full_text: string; // ... entities: { // <-- Here (a dependent Media entities) media: Media[]; hashtags: Hashtag[]; urls: Url[]; user_mentions: UserMention[]; }; }; }; // A User entity type User = { __typename: 'User'; id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2' // ... legacy: { location: string; // 'San Francisco' name: string; // 'John Doe' // ... }; }; // A Media entity type Media = { // ... source_user_id_str: string; // '1867041249938530657' <-- Here (the dependant user is being mentioned by its ID) url: string; // 'https://t.co/X78dBgtrsNU' features: { large: { faces: FaceGeometry[] }; medium: { faces: FaceGeometry[] }; small: { faces: FaceGeometry[] }; orig: { faces: FaceGeometry[] }; }; sizes: { large: MediaSize; medium: MediaSize; small: MediaSize; thumb: MediaSize; }; video_info: VideoInfo[]; }; Αυτό που είναι ενδιαφέρον εδώ είναι ότι τα περισσότερα από τα εξαρτημένα δεδομένα όπως και ενσωματώνονται στην απάντηση κατά την πρώτη κλήση (χωρίς επόμενα ερωτήματα). tweet → media tweet → author Επίσης, οι συνδέσεις και με οντότητες δεν κανονικοποιούνται (αν δύο tweets έχουν τον ίδιο συγγραφέα, τα δεδομένα τους θα επαναλαμβάνονται σε κάθε αντικείμενο tweet). Αλλά φαίνεται ότι θα έπρεπε να είναι εντάξει, αφού στο πλαίσιο του αρχικού χρονοδιαγράμματος για έναν συγκεκριμένο χρήστη τα tweets θα συντάσσονται από πολλούς συντάκτες και οι επαναλήψεις είναι πιθανές αλλά αραιές. User Media Tweet Η υπόθεσή μου ήταν ότι το API (που δεν καλύπτουμε εδώ), το οποίο είναι υπεύθυνο για την ανάκτηση των tweets θα το χειριστεί διαφορετικά, αλλά, προφανώς, δεν ισχύει. Το επιστρέφει τη λίστα των tweet του ίδιου χρήστη και ενσωματώνει τα ίδια δεδομένα χρήστη ξανά και ξανά για κάθε tweet. Είναι ενδιαφέρον. Ίσως η απλότητα της προσέγγισης ξεπερνάει κάποιο μέγεθος δεδομένων (ίσως τα δεδομένα χρήστη να θεωρούνται αρκετά μικρά σε μέγεθος). Δεν είμαι σίγουρος. UserTweets ενός συγκεκριμένου χρήστη UserTweets Μια άλλη παρατήρηση σχετικά με τη σχέση των οντοτήτων είναι ότι η οντότητα έχει επίσης έναν σύνδεσμο με τον (τον συγγραφέα). Αλλά το κάνει όχι μέσω άμεσης ενσωμάτωσης οντοτήτων όπως κάνει η οντότητα , αλλά μάλλον συνδέεται μέσω της ιδιότητας . Media User Tweet Media.source_user_id_str Τα "σχόλια" (που είναι και τα "tweets" από τη φύση τους) για κάθε "tweet" στο χρονοδιάγραμμα της αρχικής σελίδας δεν λαμβάνονται καθόλου. Για να δει το νήμα του tweet, ο χρήστης πρέπει να κάνει κλικ στο tweet για να δει τη λεπτομερή προβολή του. Το νήμα του tweet θα ληφθεί καλώντας το τελικό σημείο (περισσότερα σχετικά στην ενότητα "Σελίδα λεπτομερειών Tweet" παρακάτω). TweetDetail Μια άλλη οντότητα που έχει κάθε είναι (π.χ. "Συστήνεται λιγότερο συχνά" ή "Δείτε λιγότερα"). Ο τρόπος αποθήκευσης των στο αντικείμενο απόκρισης είναι διαφορετικός από τον τρόπο αποθήκευσης των αντικειμένων και . Ενώ οι οντότητες και αποτελούν μέρος του , οι αποθηκεύονται χωριστά στον πίνακα και συνδέονται μέσω του . Αυτό ήταν μια μικρή έκπληξη για μένα, καθώς δεν φαίνεται να ισχύει ότι οποιαδήποτε ενέργεια μπορεί να επαναχρησιμοποιηθεί. Φαίνεται ότι μια ενέργεια χρησιμοποιείται μόνο για ένα συγκεκριμένο tweet. Φαίνεται λοιπόν ότι τα θα μπορούσαν να ενσωματωθούν σε κάθε tweet με τον ίδιο τρόπο όπως οι οντότητες . Αλλά μπορεί να μου λείπει κάποια κρυμμένη πολυπλοκότητα εδώ (όπως το γεγονός ότι κάθε δράση μπορεί να έχει παιδικές ενέργειες). Tweet FeedbackActions FeedbackActions User Media User Media Tweet FeedbackActions TimelineItem.content.feedbackInfo.feedbackKeys ActionKey FeedbackActions Media Περισσότερες λεπτομέρειες σχετικά με τις ενέργειες βρίσκονται στην ενότητα "Ενέργειες Tweet" παρακάτω. Ταξινόμηση Η σειρά ταξινόμησης των καταχωρήσεων της γραμμής χρόνου ορίζεται από το backend μέσω των ιδιοτήτων : sortIndex type TimelineCursor = { entryId: string; sortIndex: string; // '1866961576813152212' <-- Here content: { __typename: 'TimelineTimelineCursor'; value: string; cursorType: 'Top' | 'Bottom'; }; }; type TimelineItem = { entryId: string; sortIndex: string; // '1866561576636152411' <-- Here content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; }; }; }; type TimelineModule = { entryId: string; sortIndex: string; // '73343543020642838441' <-- Here content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, item: TimelineTweet, }[], displayType: 'VerticalConversation', }; }; Το ίδιο μπορεί να μοιάζει κάπως με αυτό το . Πιθανότατα αντιστοιχεί άμεσα ή προέρχεται από το α . sortIndex '1867231621095096312' Ταυτότητα χιονονιφάδας . Στην πραγματικότητα, τα περισσότερα από τα αναγνωριστικά που βλέπετε στην απάντηση (αναγνωριστικά tweet) ακολουθούν τη σύμβαση "Snowflake ID" και μοιάζουν με '1867231621095096312' Εάν αυτό χρησιμοποιείται για την ταξινόμηση οντοτήτων όπως τα tweets, το σύστημα αξιοποιεί την εγγενή χρονολογική ταξινόμηση των αναγνωριστικών Snowflake. Τα tweets ή τα αντικείμενα με υψηλότερη τιμή sortIndex (πιο πρόσφατη χρονική σήμανση) εμφανίζονται υψηλότερα στη ροή, ενώ αυτά με χαμηλότερες τιμές (παλαιότερη χρονική σήμανση) εμφανίζονται χαμηλότερα στη ροή. Ακολουθεί η αποκωδικοποίηση βήμα προς βήμα του Snowflake ID (στην περίπτωσή μας το ) : sortIndex 1867231621095096312 Εξαγάγετε τη : χρονική σήμανση Η χρονική σήμανση προκύπτει μετατοπίζοντας δεξιά κατά 22 bit το Snowflake ID (για να αφαιρέσετε τα χαμηλότερα 22 bit για το κέντρο δεδομένων, το αναγνωριστικό εργαζόμενου και την ακολουθία): 1867231621095096312 → 445182709954 Προσθέστε : την εποχή του Twitter Η προσθήκη της προσαρμοσμένης εποχής του Twitter (1288834974657) σε αυτήν τη χρονική σήμανση δίνει τη χρονική σήμανση UNIX σε χιλιοστά του δευτερολέπτου: 445182709954 + 1288834974657 → 1734017684611ms Μετατροπή σε : αναγνώσιμη από τον άνθρωπο ημερομηνία Η μετατροπή της χρονικής σφραγίδας UNIX σε ώρα ημερομηνίας UTC δίνει: 1734017684611ms → 2024-12-12 15:34:44.611 (UTC) Μπορούμε λοιπόν να υποθέσουμε εδώ ότι τα tweets στο homeline είναι ταξινομημένα χρονολογικά. Ενέργειες tweet Κάθε tweet έχει ένα μενού "Ενέργειες". Οι ενέργειες για κάθε tweet προέρχονται από το backend σε έναν πίνακα και συνδέονται με τα tweet μέσω του : TimelineItem.content.feedbackInfo.feedbackKeys ActionKey type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; // <-- Here }; }; }; }; }; type TimelineItem = { entryId: string; sortIndex: string; content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] <-- Here }; }; }; type TimelineAction = { key: ActionKey; // '-609233128' value: { feedbackType: 'NotRelevant' | 'DontLike' | 'SeeFewer'; // ... prompt: string; // 'This post isn't relevant' | 'Not interested in this post' | ... confirmation: string; // 'Thanks. You'll see fewer posts like this.' childKeys: ActionKey[]; // ['1192182653', '-1427553257'], ie NotInterested -> SeeFewer feedbackUrl: string; // '/2/timeline/feedback.json?feedback_type=NotRelevant&action_metadata=SRwW6oXZadPHiOczBBaAwPanEwE%3D' hasUndoAction: boolean; icon: string; // 'Frown' }; }; Είναι ενδιαφέρον εδώ ότι αυτός ο επίπεδος πίνακας ενεργειών είναι στην πραγματικότητα ένα δέντρο (ή ένα γράφημα; Δεν το έλεγξα), καθώς κάθε ενέργεια μπορεί να έχει θυγατρικές ενέργειες (δείτε τον πίνακα ). Αυτό είναι λογικό, για παράδειγμα, όταν ο χρήστης κάνει κλικ στην ενέργεια , η συνέχεια μπορεί να είναι να εμφανιστεί η ενέργεια , ως ένας τρόπος να εξηγήσει γιατί ο χρήστης Δεν μου αρέσει το tweet. TimelineAction.value.childKeys "Δεν μου αρέσει" "Αυτή η ανάρτηση δεν είναι σχετική" Σελίδα λεπτομερειών tweet Μόλις ο χρήστης θέλει να δει τη σελίδα λεπτομερειών του tweet (δηλαδή για να δει το νήμα των σχολίων/tweet), ο χρήστης κάνει κλικ στο tweet και εκτελείται το αίτημα στο ακόλουθο τελικό σημείο: GET GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867231621095096312","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true} Ήμουν περίεργος εδώ γιατί λαμβάνεται η λίστα των tweet μέσω της κλήσης , αλλά κάθε λεπτομέρεια του tweet λαμβάνεται μέσω της κλήσης . Φαίνεται ασυνεπής. Ειδικά λαμβάνοντας υπόψη ότι παρόμοιες παράμετροι ερωτήματος, όπως , και άλλες, αυτή τη φορά μεταβιβάζονται στη διεύθυνση URL και όχι στο σώμα του αιτήματος. Η μορφή απόκρισης είναι επίσης παρόμοια και χρησιμοποιεί ξανά τους τύπους από την κλήση λίστας. Δεν είμαι σίγουρος γιατί είναι αυτό. Αλλά και πάλι, είμαι βέβαιος ότι μπορεί να μου λείπει κάποια πολυπλοκότητα του φόντου εδώ. POST GET query-id features Ακολουθούν οι απλοποιημένοι τύποι σωμάτων απόκρισης: type TweetDetailResponse = { data: { threaded_conversation_with_injections_v2: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[], }, }, } type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineTerminateTimeline = { type: 'TimelineTerminateTimeline', direction: 'Top', } type TimelineModule = { entryId: string; // 'conversationthread-58668734545929871193' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, // 'conversationthread-1866876425669871193-tweet-1866876038930951193' item: TimelineTweet, }[], // Comments to the tweets are also tweets displayType: 'VerticalConversation', }; }; Η απόκριση είναι αρκετά παρόμοια (στα είδη της) με την απόκριση της λίστας, οπότε δεν θα μείνουμε για πολύ εδώ. Μια ενδιαφέρουσα απόχρωση είναι ότι τα "σχόλια" (ή οι συνομιλίες) κάθε tweet είναι στην πραγματικότητα άλλα tweets (δείτε τον τύπο ). Έτσι, το νήμα του tweet μοιάζει πολύ με τη ροή του χρονοδιαγράμματος αρχικής σελίδας, εμφανίζοντας τη λίστα με τις καταχωρήσεις . Αυτό φαίνεται κομψό. Ένα καλό παράδειγμα μιας καθολικής και επαναχρησιμοποιήσιμης προσέγγισης στο σχεδιασμό του API. TimelineModule TimelineTweet Μου αρέσει το tweet Όταν ένας χρήστης αρέσει στο tweet, εκτελείται το αίτημα στο ακόλουθο τελικό σημείο: POST POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet Ακολουθούν οι τύποι σώματος : του αιτήματος type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N61twFgted2EgXILM7A' }; Εδώ είναι οι τύποι σώματος : απόκρισης type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } } Φαίνεται απλό και μοιάζει επίσης με την προσέγγιση που μοιάζει με RPC στο σχεδιασμό API. Σύναψη Έχουμε αγγίξει ορισμένα βασικά μέρη του σχεδιασμού του API της αρχικής γραμμής χρόνου εξετάζοντας το παράδειγμα API του X. Έκανα κάποιες υποθέσεις στην πορεία, όσο καλύτερα γνωρίζω. Πιστεύω ότι κάποια πράγματα μπορεί να έχω ερμηνεύσει λάθος και μπορεί να έχω παραλείψει κάποιες σύνθετες αποχρώσεις. Αλλά ακόμα και με αυτό κατά νου, ελπίζω να έχετε μερικές χρήσιμες πληροφορίες από αυτήν την επισκόπηση υψηλού επιπέδου, κάτι που θα μπορούσατε να εφαρμόσετε στην επόμενη συνεδρία σχεδίασης API. Αρχικά, είχα ένα σχέδιο να περάσω από παρόμοιους ιστότοπους κορυφαίας τεχνολογίας για να λάβω κάποιες πληροφορίες από το Facebook, το Reddit, το YouTube και άλλα και να συλλέξω βέλτιστες πρακτικές και λύσεις δοκιμασμένες στη μάχη. Δεν είμαι σίγουρος αν θα βρω τον χρόνο να το κάνω. Θα δεις. Αλλά θα μπορούσε να είναι μια ενδιαφέρουσα άσκηση. Παράρτημα: Όλοι οι τύποι σε ένα μέρος Για την αναφορά, προσθέτω όλους τους τύπους με μια κίνηση εδώ. Μπορείτε επίσης να βρείτε όλους τους τύπους σε αρχείο. τύπους/χ.τσ /** * This file contains the simplified types for X's (Twitter's) home timeline API. * * These types are created for exploratory purposes, to see the current implementation * of the X's API, to see how they fetch Home Feed, how they do a pagination and sorting, * and how they pass the hierarchical entities (posts, media, user info, etc). * * Many properties and types are omitted for simplicity. */ // POST https://x.com/i/api/graphql/{query-id}/HomeTimeline export type TimelineRequest = { queryId: string; // 's6ERr1UxkxxBx4YundNsXw' variables: { count: number; // 20 cursor?: string; // 'DAAACgGBGedb3Vx__9sKAAIZ5g4QENc99AcAAwAAIAIAAA' seenTweetIds: string[]; // ['1867041249938530657', '1867041249938530658'] }; features: Features; }; // POST https://x.com/i/api/graphql/{query-id}/HomeTimeline export type TimelineResponse = { data: { home: { home_timeline_urt: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[]; responseObjects: { feedbackActions: TimelineAction[]; }; }; }; }; }; // POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet export type FavoriteTweetRequest = { variables: { tweet_id: string; // '1867041249938530657' }; queryId: string; // 'lI07N6OtwFgted2EgXILM7A' }; // POST https://x.com/i/api/graphql/{query-id}/FavoriteTweet export type FavoriteTweetResponse = { data: { favorite_tweet: 'Done', } } // GET https://x.com/i/api/graphql/{query-id}/TweetDetail?variables={"focalTweetId":"1867041249938530657","referrer":"home","controller_data":"DACABBSQ","rankingMode":"Relevance","includePromotedContent":true,"withCommunity":true}&features={"articles_preview_enabled":true} export type TweetDetailResponse = { data: { threaded_conversation_with_injections_v2: { instructions: (TimelineAddEntries | TimelineTerminateTimeline)[], }, }, } type Features = { articles_preview_enabled: boolean; view_counts_everywhere_api_enabled: boolean; // ... } type TimelineAction = { key: ActionKey; // '-609233128' value: { feedbackType: 'NotRelevant' | 'DontLike' | 'SeeFewer'; // ... prompt: string; // 'This post isn't relevant' | 'Not interested in this post' | ... confirmation: string; // 'Thanks. You'll see fewer posts like this.' childKeys: ActionKey[]; // ['1192182653', '-1427553257'], ie NotInterested -> SeeFewer feedbackUrl: string; // '/2/timeline/feedback.json?feedback_type=NotRelevant&action_metadata=SRwW6oXZadPHiOczBBaAwPanEwE%3D' hasUndoAction: boolean; icon: string; // 'Frown' }; }; type TimelineAddEntries = { type: 'TimelineAddEntries'; entries: (TimelineItem | TimelineCursor | TimelineModule)[]; }; type TimelineTerminateTimeline = { type: 'TimelineTerminateTimeline', direction: 'Top', } type TimelineCursor = { entryId: string; // 'cursor-top-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineCursor'; value: string; // 'DACBCgABGedb4VyaJwuKbIIZ40cX3dYwGgaAAwAEAEEAA' cursorType: 'Top' | 'Bottom'; }; }; type TimelineItem = { entryId: string; // 'tweet-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineItem'; itemContent: TimelineTweet; feedbackInfo: { feedbackKeys: ActionKey[]; // ['-1378668161'] }; }; }; type TimelineModule = { entryId: string; // 'conversationthread-1867041249938530657' sortIndex: string; // '1867231621095096312' content: { __typename: 'TimelineTimelineModule'; items: { entryId: string, // 'conversationthread-1867041249938530657-tweet-1867041249938530657' item: TimelineTweet, }[], // Comments to the tweets are also tweets displayType: 'VerticalConversation', }; }; type TimelineTweet = { __typename: 'TimelineTweet'; tweet_results: { result: Tweet; }; }; type Tweet = { __typename: 'Tweet'; core: { user_results: { result: User; }; }; views: { count: string; // '13763' }; legacy: { bookmark_count: number; // 358 created_at: string; // 'Tue Dec 10 17:41:28 +0000 2024' conversation_id_str: string; // '1867041249938530657' display_text_range: number[]; // [0, 58] favorite_count: number; // 151 full_text: string; // "How I'd promote my startup, if I had 0 followers (Part 1)" lang: string; // 'en' quote_count: number; reply_count: number; retweet_count: number; user_id_str: string; // '1867041249938530657' id_str: string; // '1867041249938530657' entities: { media: Media[]; hashtags: Hashtag[]; urls: Url[]; user_mentions: UserMention[]; }; }; }; type User = { __typename: 'User'; id: string; // 'VXNlcjoxNDUxM4ADSG44MTA4NDc4OTc2' rest_id: string; // '1867041249938530657' is_blue_verified: boolean; profile_image_shape: 'Circle'; // ... legacy: { following: boolean; created_at: string; // 'Thu Oct 21 09:30:37 +0000 2021' description: string; // 'I help startup founders double their MRR with outside-the-box marketing cheat sheets' favourites_count: number; // 22195 followers_count: number; // 25658 friends_count: number; location: string; // 'San Francisco' media_count: number; name: string; // 'John Doe' profile_banner_url: string; // 'https://pbs.twimg.com/profile_banners/4863509452891265813/4863509' profile_image_url_https: string; // 'https://pbs.twimg.com/profile_images/4863509452891265813/4863509_normal.jpg' screen_name: string; // 'johndoe' url: string; // 'https://t.co/dgTEddFGDd' verified: boolean; }; }; type Media = { display_url: string; // 'pic.x.com/X7823zS3sNU' expanded_url: string; // 'https://x.com/johndoe/status/1867041249938530657/video/1' ext_alt_text: string; // 'Image of two bridges.' id_str: string; // '1867041249938530657' indices: number[]; // [93, 116] media_key: string; // '13_2866509231399826944' media_url_https: string; // 'https://pbs.twimg.com/profile_images/1867041249938530657/4863509_normal.jpg' source_status_id_str: string; // '1867041249938530657' source_user_id_str: string; // '1867041249938530657' type: string; // 'video' url: string; // 'https://t.co/X78dBgtrsNU' features: { large: { faces: FaceGeometry[] }; medium: { faces: FaceGeometry[] }; small: { faces: FaceGeometry[] }; orig: { faces: FaceGeometry[] }; }; sizes: { large: MediaSize; medium: MediaSize; small: MediaSize; thumb: MediaSize; }; video_info: VideoInfo[]; }; type UserMention = { id_str: string; // '98008038' name: string; // 'Yann LeCun' screen_name: string; // 'ylecun' indices: number[]; // [115, 122] }; type Hashtag = { indices: number[]; // [257, 263] text: string; }; type Url = { display_url: string; // 'google.com' expanded_url: string; // 'http://google.com' url: string; // 'https://t.co/nZh3aF0Aw6' indices: number[]; // [102, 125] }; type VideoInfo = { aspect_ratio: number[]; // [427, 240] duration_millis: number; // 20000 variants: { bitrate?: number; // 288000 content_type?: string; // 'application/x-mpegURL' | 'video/mp4' | ... url: string; // 'https://video.twimg.com/amplify_video/18665094345456w6944/pl/-ItQau_LRWedR-W7.m3u8?tag=14' }; }; type FaceGeometry = { x: number; y: number; h: number; w: number }; type MediaSize = { h: number; w: number; resize: 'fit' | 'crop' }; type ActionKey = string;