Github.tcl

Text Detected Guest 5 Views Size: 11.76 KB Posted on: Feb 16, 26 @ 10:24 PM
  1. # github-multi.tcl
  2. # Supports ANY public owner/repo via !gh commands
  3. # Polls and auto-announces only the defined repos
  4.  
  5. namespace eval ::github {
  6.     variable cfg
  7.     variable state
  8.     variable repos
  9.     variable cmd_cooldown
  10.     variable default_repo "boxlabss/PASTE"
  11.  
  12.     array set cfg {
  13.         poll_interval     120
  14.         token             "GITHUBTOKENHERE"
  15.         per_page          5
  16.         timeout_ms        10000
  17.         state_file        "github_state.txt"
  18.         announce_on_start 0
  19.         cmd_limit         3
  20.     }
  21.  
  22.     array set repos {
  23.         "boxlabss/PASTE"    {master   "#PASTE"}
  24.         "boxlabss/HexDroid" {main     "#HexDroid"}
  25.     }
  26.  
  27.     # map for monitored repos
  28.     array set repo_map {}
  29.     foreach r [array names repos] {
  30.         set repo_map([string tolower $r]) $r
  31.     }
  32.  
  33.     array set state {}
  34.     array set cmd_cooldown {}
  35. }
  36.  
  37. # DEPENDENCIES
  38. package require http
  39. package require tls
  40. package require json
  41.  
  42. ::tls::init -ssl2 0 -ssl3 0 -tls1 1 -tls1.1 1 -tls1.2 1 -tls1.3 1
  43. ::http::register https 443 ::tls::socket
  44. ::http::config -useragent "eggdrop-github-multi/1.6"
  45.  
  46. # SAVE / LOAD STATE
  47. proc ::github::save_state {} {
  48.     variable cfg
  49.     variable state
  50.     set f [open $cfg(state_file) w]
  51.     puts $f [array get state]
  52.     close $f
  53. }
  54.  
  55. proc ::github::load_state {} {
  56.     variable cfg
  57.     variable state
  58.     if {![file exists $cfg(state_file)]} return
  59.     set f [open $cfg(state_file) r]
  60.     catch {array set state [read $f]}
  61.     close $f
  62. }
  63.  
  64. # HTTP GET WRAPPER
  65. proc ::github::http_get {url {extraHeaders {}}} {
  66.     variable cfg
  67.     set headers [list Accept "application/vnd.github+json" User-Agent "eggdrop-github-multi/1.6"]
  68.     if {[string length $cfg(token)]} { lappend headers Authorization "Bearer $cfg(token)" }
  69.     if {[llength $extraHeaders]} { set headers [concat $headers $extraHeaders] }
  70.  
  71.     set tok [::http::geturl $url -headers $headers -timeout $cfg(timeout_ms)]
  72.     set code [lindex [split [::http::code $tok]] 1]
  73.     set body [::http::data $tok]
  74.     set meta [::http::meta $tok]
  75.     array set H {}
  76.     foreach {k v} $meta { set H([string tolower $k]) $v }
  77.     ::http::cleanup $tok
  78.     return [list $code $body [array get H]]
  79. }
  80.  
  81. proc ::github::say {channel msg} {
  82.     if {$channel eq ""} return
  83.     putserv "PRIVMSG $channel :$msg"
  84. }
  85.  
  86. proc ::github::shortmsg {text maxlen} {
  87.     set line [string trim [lindex [split $text \n] 0]]
  88.     if {[string length $line] > $maxlen} {
  89.         set line "[string range $line 0 [expr {$maxlen-4}]]..."
  90.     }
  91.     return $line
  92. }
  93.  
  94. # DATA FETCHERS
  95. proc ::github::fetch_latest_commit {repo branch} {
  96.     set url "https://api.github.com/repos/$repo/commits?per_page=1&sha=$branch"
  97.     lassign [::github::http_get $url] code body H
  98.     if {$code < 200 || $code >= 300} { return {} }
  99.     set data [::json::json2dict $body]
  100.     if {![llength $data]} { return {} }
  101.     set item [lindex $data 0]
  102.  
  103.     set sha    [dict get $item sha]
  104.     set commit [dict get $item commit]
  105.     set msg    [dict get $commit message]
  106.     set url    [dict get $item html_url]
  107.  
  108.     if {[dict exists $item author] && [dict get $item author] ne ""} {
  109.         set author [dict get [dict get $item author] login]
  110.     } else {
  111.         set author [dict get [dict get $commit author] name]
  112.     }
  113.  
  114.     return [list sha $sha author $author message $msg url $url]
  115. }
  116.  
  117. proc ::github::fetch_commits {repo branch} {
  118.     variable cfg
  119.     variable state
  120.     set url "https://api.github.com/repos/$repo/commits?per_page=$cfg(per_page)&sha=$branch"
  121.     set headers {}
  122.     if {[info exists state($repo,etag_commits)]} {
  123.         lappend headers If-None-Match $state($repo,etag_commits)
  124.     }
  125.     lassign [::github::http_get $url $headers] code body H
  126.     if {$code == 304} { return {} }
  127.     if {$code < 200 || $code >= 300} { return {} }
  128.     if {[dict exists $H etag]} { set state($repo,etag_commits) [dict get $H etag] }
  129.  
  130.     set data [::json::json2dict $body]
  131.     set out {}
  132.     foreach item $data {
  133.         set sha [dict get $item sha]
  134.         if {[info exists state($repo,last_commit_sha)] && $sha eq $state($repo,last_commit_sha)} break
  135.  
  136.         set commit [dict get $item commit]
  137.         set msg    [dict get $commit message]
  138.         set url    [dict get $item html_url]
  139.  
  140.         if {[dict exists $item author] && [dict get $item author] ne ""} {
  141.             set author [dict get [dict get $item author] login]
  142.         } else {
  143.             set author [dict get [dict get $commit author] name]
  144.         }
  145.  
  146.         lappend out [list sha $sha author $author message $msg url $url]
  147.     }
  148.     return $out
  149. }
  150.  
  151. proc ::github::fetch_issues {repo {limit 0}} {
  152.     variable cfg
  153.     if {$limit == 0} { set limit $cfg(per_page) }
  154.     set url "https://api.github.com/repos/$repo/issues?state=open&sort=created&direction=desc&per_page=$limit"
  155.     lassign [::github::http_get $url] code body H
  156.     if {$code < 200 || $code >= 300} { return {} }
  157.     set data [::json::json2dict $body]
  158.     set out {}
  159.     foreach item $data {
  160.         if {[dict exists $item pull_request]} continue
  161.         lappend out [list id [dict get $item id] number [dict get $item number] \
  162.                           title [dict get $item title] user [dict get [dict get $item user] login] \
  163.                           url [dict get $item html_url]]
  164.     }
  165.     return $out
  166. }
  167.  
  168. # ANNOUNCEMENTS
  169. proc ::github::announce_commits {repo branch channel commits} {
  170.     variable state
  171.     foreach c [lreverse $commits] {
  172.         array set C $c
  173.         set msg [::github::shortmsg $C(message) 140]
  174.         ::github::say $channel "🧩 $repo@$branch: $C(author) committed β€œ$msg” β€” $C(url)"
  175.         set state($repo,last_commit_sha) $C(sha)
  176.     }
  177. }
  178.  
  179. proc ::github::announce_issues {repo channel issues} {
  180.     variable state
  181.     if {![info exists state($repo,seen_issue_ids)]} { set state($repo,seen_issue_ids) {} }
  182.     foreach i [lreverse $issues] {
  183.         array set I $i
  184.         if {[lsearch -exact $state($repo,seen_issue_ids) $I(id)] >= 0} continue
  185.         ::github::say $channel "🐞 New issue #$I(number) by $I(user): $I(title) β€” $I(url)"
  186.         lappend state($repo,seen_issue_ids) $I(id)
  187.         if {[llength $state($repo,seen_issue_ids)] > 200} {
  188.             set state($repo,seen_issue_ids) [lrange $state($repo,seen_issue_ids) end-199 end]
  189.         }
  190.     }
  191. }
  192.  
  193. # POLLING
  194. proc ::github::poll {} {
  195.     variable repos
  196.     variable cfg
  197.     foreach repo [array names repos] {
  198.         lassign $repos($repo) branch channel
  199.         set commits [::github::fetch_commits $repo $branch]
  200.         if {[llength $commits]} { ::github::announce_commits $repo $branch $channel $commits }
  201.         set issues [::github::fetch_issues $repo]
  202.         if {[llength $issues]} { ::github::announce_issues $repo $channel $issues }
  203.     }
  204.     ::github::save_state
  205.     utimer $cfg(poll_interval) ::github::poll
  206. }
  207.  
  208. # COMMAND HELPERS
  209. proc ::github::cooldown_ok {chan} {
  210.     variable cmd_cooldown
  211.     set now [unixtime]
  212.     if {[info exists cmd_cooldown($chan)] && ($now - $cmd_cooldown($chan)) < 8} { return 0 }
  213.     set cmd_cooldown($chan) $now
  214.     return 1
  215. }
  216.  
  217. proc ::github::get_canonical_repo {input} {
  218.     variable repo_map
  219.     set lower [string tolower $input]
  220.     return [expr {[info exists repo_map($lower)] ? $repo_map($lower) : $input}]
  221. }
  222.  
  223. proc ::github::get_default_branch {repo} {
  224.     set url "https://api.github.com/repos/$repo"
  225.     lassign [::github::http_get $url] code body H
  226.     if {$code != 200} {
  227.         putlog "github: failed to get default branch for $repo (HTTP $code)"
  228.         return "main"
  229.     }
  230.     set data [::json::json2dict $body]
  231.     if {[dict exists $data default_branch]} {
  232.         return [dict get $data default_branch]
  233.     }
  234.     return "main"
  235. }
  236.  
  237. proc ::github::get_branch {repo} {
  238.     variable repos
  239.     if {[info exists repos($repo)]} {
  240.         return [lindex $repos($repo) 0]
  241.     }
  242.     return [::github::get_default_branch $repo]
  243. }
  244.  
  245. # USER COMMANDS
  246. proc ::github::cmd_latest {chan repo} {
  247.     set branch [::github::get_branch $repo]
  248.     set latest [::github::fetch_latest_commit $repo $branch]
  249.     if {[llength $latest]} {
  250.         array set C $latest
  251.         set msg [::github::shortmsg $C(message) 100]
  252.         putserv "PRIVMSG $chan :🧩 Latest commit: $repo@$branch β†’ $C(author): $msg β€” $C(url)"
  253.     } else {
  254.         putserv "PRIVMSG $chan :❌ Could not fetch latest commit for $repo (repo may not exist, be private, or rate-limited)"
  255.     }
  256.  
  257.     set issues [::github::fetch_issues $repo 1]
  258.     if {[llength $issues]} {
  259.         array set I [lindex $issues 0]
  260.         putserv "PRIVMSG $chan :🐞 Latest open issue: #$I(number) by $I(user): $I(title) β€” $I(url)"
  261.     }
  262. }
  263.  
  264. proc ::github::cmd_commits {chan repo} {
  265.     variable cfg
  266.     set branch [::github::get_branch $repo]
  267.     putserv "PRIVMSG $chan :🧩 Last $cfg(cmd_limit) commits for $repo@$branch:"
  268.     set url "https://api.github.com/repos/$repo/commits?per_page=$cfg(cmd_limit)&sha=$branch"
  269.     lassign [::github::http_get $url] code body H
  270.     if {$code < 200 || $code >= 300} {
  271.         putserv "PRIVMSG $chan :❌ Error fetching commits (HTTP $code) β€” repo may not exist or be private"
  272.         return
  273.     }
  274.     set data [::json::json2dict $body]
  275.     if {![llength $data]} {
  276.         putserv "PRIVMSG $chan :No commits found on branch $branch"
  277.         return
  278.     }
  279.     foreach item $data {
  280.         set commit [dict get $item commit]
  281.         set msg [::github::shortmsg [dict get $commit message] 80]
  282.         set url [dict get $item html_url]
  283.         putserv "PRIVMSG $chan :β€’ $msg β€” $url"
  284.     }
  285. }
  286.  
  287. proc ::github::cmd_issues {chan repo} {
  288.     variable cfg
  289.     putserv "PRIVMSG $chan :🐞 Last $cfg(cmd_limit) open issues for $repo:"
  290.     set issues [::github::fetch_issues $repo $cfg(cmd_limit)]
  291.     if {![llength $issues]} {
  292.         putserv "PRIVMSG $chan :No open issues found (repo may not exist, be private, or have none open)."
  293.         return
  294.     }
  295.     foreach i $issues {
  296.         array set I $i
  297.         putserv "PRIVMSG $chan :β€’ #$I(number): $I(title) β€” $I(url)"
  298.     }
  299. }
  300.  
  301. proc ::github::cmd_dispatch {nick host hand chan text} {
  302.     variable default_repo
  303.  
  304.     if {![::github::cooldown_ok $chan]} return
  305.  
  306.     set args [split [string trim $text]]
  307.     set sub  [string tolower [lindex $args 0]]
  308.     set input [lindex $args 1]
  309.  
  310.     if {$input eq ""} {
  311.         set repo $default_repo
  312.     } else {
  313.         set repo [::github::get_canonical_repo $input]
  314.     }
  315.  
  316.     switch -- $sub {
  317.         latest   { ::github::cmd_latest   $chan $repo }
  318.         commits  { ::github::cmd_commits  $chan $repo }
  319.         issues   { ::github::cmd_issues   $chan $repo }
  320.         default  {
  321.             putserv "PRIVMSG $chan :Usage: !gh <latest|commits|issues> \[owner/repo\]"
  322.             putserv "PRIVMSG $chan :Monitored (auto-announce): [join [array names ::github::repos] ", "]"
  323.             putserv "PRIVMSG $chan :Any public repo works for on-demand queries (e.g. !gh commits evilnet/x3)"
  324.         }
  325.     }
  326. }
  327.  
  328. bind pub - "!gh" ::github::cmd_dispatch
  329. proc ::github::init {event} {
  330.     ::github::load_state
  331.     if {!$::github::cfg(announce_on_start)} {
  332.         foreach repo [array names ::github::repos] {
  333.             lassign $::github::repos($repo) branch channel
  334.             set first [::github::fetch_latest_commit $repo $branch]
  335.             if {[llength $first]} {
  336.                 array set C $first
  337.                 set ::github::state($repo,last_commit_sha) $C(sha)
  338.             }
  339.             set issues [::github::fetch_issues $repo 5]
  340.             foreach i $issues {
  341.                 array set I $i
  342.                 lappend ::github::state($repo,seen_issue_ids) $I(id)
  343.             }
  344.         }
  345.         ::github::save_state
  346.     }
  347.     putlog "github-multi: monitoring started"
  348.     utimer 5 ::github::poll
  349. }
  350.  
  351. bind evnt - init-server ::github::init

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and β€” with your consent β€” to measure usage and show ads. See Privacy.