זרימת שיתוף ב-Gist (Gist Flow)

summary:

נקודות הכניסה לשיתוף ב-Gist, למה ה-Gist נוצר תחת חשבון ה-GitHub של המשתמש ולא של המערכת, מצבי auth_failed, וההתנהגות fail-closed בכל מסלול כשל.

סקירה כללית

שיתוף ב-GitHub Gist יוצר את ה-Gist תחת החשבון האישי של המשתמש, לא תחת חשבון המערכת. זו לא החלטה קוסמטית: Gist שנוצר בטוקן המערכת מופיע ברשימת ה-Gists של המערכת, מיוחס אליה, והמשתמש לא יכול לערוך או למחוק אותו. לכן GITHUB_TOKEN (משתנה הסביבה) אינו משמש ליצירת Gist באף מסלול — גם לא כמסלול נפילה בכשל.

המשמעות המעשית: משתמש שלא חיבר חשבון GitHub לא יכול לשתף ב-Gist, ומקבל הנחיה לחבר או להשתמש ב-Pastebin.

נקודות כניסה

ארבעה מסלולים מגיעים לאותה פונקציה, integrations.resolve_gist_for_user:

מסלולי הכניסה

פונקציה

מודול

הקשר

מתודת היצירה

share_single_by_id

conversation_handlers

שיתוף קובץ בודד מתוך שיחה

create_gist

_share_to_gist

bot_handlers

כפתור 🔗 שתף קוד על קובץ

create_gist

_share_to_gist_multi

bot_handlers

שיתוף מרובה קבצים

create_gist_multi

_export_gist

refactor_handlers

ייצוא תוצאת ריפקטורינג

create_gist_multi

כל ארבעתם קוראים דרך asyncio.to_thread. הקריאה חוסמת — היא נוגעת ב-MongoDB וברשת — ומתוך handler אסינכרוני היא הייתה תוקעת את ה-event loop לכל המשתמשים, לא רק למי שלחץ.

זרימת עבודה בסיסית

        sequenceDiagram
    participant U as User
    participant H as Handler
    participant R as resolve_gist_for_user
    participant DB as MongoDB
    participant I as GitHubGistIntegration
    participant GH as GitHub API

    U->>H: 🔗 שתף קוד ← 🐙 GitHub Gist
    H->>R: asyncio.to_thread(resolve_gist_for_user, user_id)
    R->>DB: get_github_token(user_id)
    DB->>R: טוקן מפוענח או None

    alt אין טוקן
      R->>H: (None, GIST_NEEDS_GITHUB_MESSAGE)
      H->>U: "כדי לשתף ב-Gist צריך לחבר GitHub"
    else יש טוקן
      R->>I: GitHubGistIntegration(token=token)
      I->>GH: GET /user (דרך גישה ל-login)
      alt הטוקן נדחה
        GH->>I: 401 / 403 / 404
        I->>I: auth_failed = True
        R->>H: (None, GIST_NEEDS_GITHUB_MESSAGE)
        H->>U: "חבר מחדש"
      else תקלה זמנית
        GH->>I: 5xx / הגבלת קצב
        I->>I: auth_failed = False
        R->>H: (None, GIST_TEMPORARY_FAILURE_MESSAGE)
        H->>U: "נסה שוב בעוד רגע"
      else הצלחה
        GH->>I: 200
        R->>H: (integration, None)
        alt קובץ בודד
          H->>I: asyncio.to_thread(create_gist, ...)
        else שיתוף מרובה או ייצוא ריפקטורינג
          H->>I: asyncio.to_thread(create_gist_multi, ...)
        end
        I->>GH: POST /gists
        alt נוצר ויש url
          GH->>I: Gist
          I->>H: dict עם url
          H->>U: הקישור ל-Gist
        else None או dict בלי url
          I->>H: None
          H->>U: "השיתוף נכשל"
        end
      end
    end
    

אימות הטוקן — למה יש שורה שנראית מיותרת

בבנאי של GitHubGistIntegration יש שורה שנראית כמו קוד מת:

user = self.github.get_user()
_ = user.login
self.user = user

היא לא מיותרת, והיא חייבת להישאר. Github.get_user() ללא ארגומנט מחזיר AuthenticatedUser עם completed=False — אובייקט עצל שלא שלח שום בקשה. ה-GET /user יוצא רק בגישה הראשונה לשדה, דרך _completeIfNotSet. בלי הגישה ל-login, טוקן שנשלל היה ”מתחבר“ בהצלחה, is_available() היה מחזיר אמת, וכל סיווג הכשלים למטה לא היה רץ לעולם. הכשל היה מתגלה רק בניסיון ליצור את ה-Gist, עם הודעה שלא מצביעה על המקור.

הערך עצמו לא נרשם ללוג: login הוא מזהה אישי. השורה הזו כבר נמחקה פעם אחת בתיקון PII, וזה הפיל את האימות — ראה side-effect-riding-on-log-line.

שלושת מצבי auth_failed

auth_failed הוא Optional[bool], ולא דגל דו-ערכי. שלושת הערכים מבחינים בין שני סוגי כשל שמחייבים הודעות שונות — ומצב שלישי שבו לא נכשלנו כלל:

מצבי auth_failed

ערך

משמעות

מה המשתמש רואה

None

לא ניסינו, או שההתחברות הצליחה

הזרימה ממשיכה

True

הטוקן עצמו פסול

GIST_NEEDS_GITHUB_MESSAGE — חבר מחדש

False

הכשל זמני; הטוקן תקין

GIST_TEMPORARY_FAILURE_MESSAGE — נסה שוב

הסיווג נעשה ב-_is_auth_failure, והוא נשען על תת-המחלקות של PyGithub ולא על קוד הסטטוס:

if isinstance(error, RateLimitExceededException):
    return False              # הטוקן תקין, המכסה נגמרה
if isinstance(error, (BadCredentialsException, TwoFactorException)):
    return True
return getattr(error, "status", None) in {401, 403, 404}

למה לא לפי קוד סטטוס בלבד: 403 הוא גם הגבלת קצב וגם הרשאה חסרה. טוקן fine-grained בלי הרשאת gist מחזיר 403 אמיתי שדורש חיבור מחדש, בעוד שמכסה שנגמרה מחזירה 403 שדורש רק המתנה. PyGithub כבר מבחין ביניהם ב-Requester.createException, ולכן נשענים על הסיווג שלו. לשלוח משתמש לחבר מחדש חשבון תקין, בגלל מכסה שתתאפס בעוד רבע שעה, זו הודעה שגורמת נזק.

למה שתי הודעות ולא אחת

זו ההבחנה המרכזית בזרימה הזו. ”לא חיברת GitHub“ ו“GitHub לא זמין כרגע“ נראים דומים בקוד ומובילים לפעולה הפוכה אצל המשתמש: הראשון דורש ממנו ללכת לתפריט ולחבר חשבון, השני דורש ממנו לא לעשות כלום ולנסות שוב. הודעה אחת גנרית הייתה שולחת משתמש מחובר לחבר חשבון שכבר מחובר — ואם הוא ”יתקן“ את זה, הוא עלול לנתק ולחבר מחדש בלי סיבה.

Fail-closed

בכל מסלול כשל התוצאה זהה מבחינת המשתמש: לא מוצג קישור, ולא מדווחת הצלחה, ואין נפילה לטוקן המערכת. זה מכוון. ה-except Exception סביב טעינת הטוקן רחב במכוון — כל כשל שכן נזרק מסתיים באי-שיתוף ולא בשיתוף תחת חשבון המערכת.

מה fail-closed כאן לא מבטיח: שה-Gist לא נוצר בצד GitHub. create_gist בונה את מילון התוצאה אחרי שה-POST /gists כבר הצליח — היא ניגשת ל-gist.created_at.isoformat() ולשדות של כל קובץ. כשל בשלב הזה נתפס באותו except ומחזיר None, בעוד שה-Gist כבר קיים בחשבון המשתמש. המשתמש יראה ”נכשל“, וניסיון חוזר ייצור Gist שני. החלון צר, אבל הוא קיים — ולכן הניסוח כאן הוא ”אין קישור“ ולא ”אין Gist“.

ה-logger.exception הוא מה שמונע בליעה שקטה: ה-traceback נרשם ללוג, ובסביבה שבה Sentry מוגדר הוא נאסף גם שם. בלי DSN, init_sentry יוצאת מוקדם ו-LoggingIntegration לא מותקנת כלל — אז ההבטחה היא הלוג, לא Sentry.

Edge Cases

``user_id`` ריק או אפס

יציאה מוקדמת עם GIST_NEEDS_GITHUB_MESSAGE, בלי פנייה ל-DB.

``create_gist`` / ``create_gist_multi`` מחזירות ``None``

שתיהן בולעות חריגות ומחזירות None בכשל — הן לא זורקות. לכן try/except סביבן לא מספיק, ובדיקת התוצאה היא מה שמונע דיווח ✅ על שיתוף שלא קרה. זה דפוס K11. _export_gist בודק if not result or not result.get("url") — כלומר גם תוצאה שחזרה אבל בלי url נחשבת כשל, כי בלי url אין מה להציג למשתמש.

כשל בטעינת הטוקן מ-MongoDB

get_github_token ב-database/repository.py תופסת את החריגה בעצמה, רושמת db_get_github_token_error ומחזירה None. כלומר תקלת DB אינה מגיעה כחריגה ל-resolve_gist_for_user, אלא נראית שם כמו ”אין טוקן“ — והמשתמש מקבל את הודעת החיבור ולא את הודעת התקלה הזמנית. ה-fail-closed נשמר, אבל ההבחנה בין שתי ההודעות מתבטלת במסלול הזה.

הטוקן מוצפן ב-DB

get_github_token מפענחת דרך secret_manager.decrypt_secret. אם הפענוח נכשל היא מחזירה את הערך המאוחסן כפי שהוא, וההתחברות ל-GitHub תיכשל עם 401 — כלומר תסווג כטוקן פסול.

קישורים