- # github-multi.tcl
- # Supports ANY public owner/repo via !gh commands
- # Polls and auto-announces only the defined repos
- namespace eval ::github {
- variable cfg
- variable state
- variable repos
- variable cmd_cooldown
- variable default_repo "boxlabss/PASTE"
- array set cfg {
- poll_interval 120
- token "GITHUBTOKENHERE"
- per_page 5
- timeout_ms 10000
- state_file "github_state.txt"
- announce_on_start 0
- cmd_limit 3
- }
- array set repos {
- "boxlabss/PASTE" {master "#PASTE"}
- "boxlabss/HexDroid" {main "#HexDroid"}
- }
- # map for monitored repos
- array set repo_map {}
- foreach r [array names repos] {
- set repo_map([string tolower $r]) $r
- }
- array set state {}
- array set cmd_cooldown {}
- }
- # DEPENDENCIES
- package require http
- package require tls
- package require json
- ::tls::init -ssl2 0 -ssl3 0 -tls1 1 -tls1.1 1 -tls1.2 1 -tls1.3 1
- ::http::register https 443 ::tls::socket
- ::http::config -useragent "eggdrop-github-multi/1.6"
- # SAVE / LOAD STATE
- proc ::github::save_state {} {
- variable cfg
- variable state
- set f [open $cfg(state_file) w]
- puts $f [array get state]
- close $f
- }
- proc ::github::load_state {} {
- variable cfg
- variable state
- if {![file exists $cfg(state_file)]} return
- set f [open $cfg(state_file) r]
- catch {array set state [read $f]}
- close $f
- }
- # HTTP GET WRAPPER
- proc ::github::http_get {url {extraHeaders {}}} {
- variable cfg
- set headers [list Accept "application/vnd.github+json" User-Agent "eggdrop-github-multi/1.6"]
- if {[string length $cfg(token)]} { lappend headers Authorization "Bearer $cfg(token)" }
- if {[llength $extraHeaders]} { set headers [concat $headers $extraHeaders] }
- set tok [::http::geturl $url -headers $headers -timeout $cfg(timeout_ms)]
- set code [lindex [split [::http::code $tok]] 1]
- set body [::http::data $tok]
- set meta [::http::meta $tok]
- array set H {}
- foreach {k v} $meta { set H([string tolower $k]) $v }
- ::http::cleanup $tok
- return [list $code $body [array get H]]
- }
- proc ::github::say {channel msg} {
- if {$channel eq ""} return
- putserv "PRIVMSG $channel :$msg"
- }
- proc ::github::shortmsg {text maxlen} {
- set line [string trim [lindex [split $text \n] 0]]
- if {[string length $line] > $maxlen} {
- set line "[string range $line 0 [expr {$maxlen-4}]]..."
- }
- return $line
- }
- # DATA FETCHERS
- proc ::github::fetch_latest_commit {repo branch} {
- set url "https://api.github.com/repos/$repo/commits?per_page=1&sha=$branch"
- lassign [::github::http_get $url] code body H
- if {$code < 200 || $code >= 300} { return {} }
- set data [::json::json2dict $body]
- if {![llength $data]} { return {} }
- set item [lindex $data 0]
- set sha [dict get $item sha]
- set commit [dict get $item commit]
- set msg [dict get $commit message]
- set url [dict get $item html_url]
- if {[dict exists $item author] && [dict get $item author] ne ""} {
- set author [dict get [dict get $item author] login]
- } else {
- set author [dict get [dict get $commit author] name]
- }
- return [list sha $sha author $author message $msg url $url]
- }
- proc ::github::fetch_commits {repo branch} {
- variable cfg
- variable state
- set url "https://api.github.com/repos/$repo/commits?per_page=$cfg(per_page)&sha=$branch"
- set headers {}
- if {[info exists state($repo,etag_commits)]} {
- lappend headers If-None-Match $state($repo,etag_commits)
- }
- lassign [::github::http_get $url $headers] code body H
- if {$code == 304} { return {} }
- if {$code < 200 || $code >= 300} { return {} }
- if {[dict exists $H etag]} { set state($repo,etag_commits) [dict get $H etag] }
- set data [::json::json2dict $body]
- set out {}
- foreach item $data {
- set sha [dict get $item sha]
- if {[info exists state($repo,last_commit_sha)] && $sha eq $state($repo,last_commit_sha)} break
- set commit [dict get $item commit]
- set msg [dict get $commit message]
- set url [dict get $item html_url]
- if {[dict exists $item author] && [dict get $item author] ne ""} {
- set author [dict get [dict get $item author] login]
- } else {
- set author [dict get [dict get $commit author] name]
- }
- lappend out [list sha $sha author $author message $msg url $url]
- }
- return $out
- }
- proc ::github::fetch_issues {repo {limit 0}} {
- variable cfg
- if {$limit == 0} { set limit $cfg(per_page) }
- set url "https://api.github.com/repos/$repo/issues?state=open&sort=created&direction=desc&per_page=$limit"
- lassign [::github::http_get $url] code body H
- if {$code < 200 || $code >= 300} { return {} }
- set data [::json::json2dict $body]
- set out {}
- foreach item $data {
- if {[dict exists $item pull_request]} continue
- lappend out [list id [dict get $item id] number [dict get $item number] \
- title [dict get $item title] user [dict get [dict get $item user] login] \
- url [dict get $item html_url]]
- }
- return $out
- }
- # ANNOUNCEMENTS
- proc ::github::announce_commits {repo branch channel commits} {
- variable state
- foreach c [lreverse $commits] {
- array set C $c
- set msg [::github::shortmsg $C(message) 140]
- ::github::say $channel "π§© $repo@$branch: $C(author) committed β$msgβ β $C(url)"
- set state($repo,last_commit_sha) $C(sha)
- }
- }
- proc ::github::announce_issues {repo channel issues} {
- variable state
- if {![info exists state($repo,seen_issue_ids)]} { set state($repo,seen_issue_ids) {} }
- foreach i [lreverse $issues] {
- array set I $i
- if {[lsearch -exact $state($repo,seen_issue_ids) $I(id)] >= 0} continue
- ::github::say $channel "π New issue #$I(number) by $I(user): $I(title) β $I(url)"
- lappend state($repo,seen_issue_ids) $I(id)
- if {[llength $state($repo,seen_issue_ids)] > 200} {
- set state($repo,seen_issue_ids) [lrange $state($repo,seen_issue_ids) end-199 end]
- }
- }
- }
- # POLLING
- proc ::github::poll {} {
- variable repos
- variable cfg
- foreach repo [array names repos] {
- lassign $repos($repo) branch channel
- set commits [::github::fetch_commits $repo $branch]
- if {[llength $commits]} { ::github::announce_commits $repo $branch $channel $commits }
- set issues [::github::fetch_issues $repo]
- if {[llength $issues]} { ::github::announce_issues $repo $channel $issues }
- }
- ::github::save_state
- utimer $cfg(poll_interval) ::github::poll
- }
- # COMMAND HELPERS
- proc ::github::cooldown_ok {chan} {
- variable cmd_cooldown
- set now [unixtime]
- if {[info exists cmd_cooldown($chan)] && ($now - $cmd_cooldown($chan)) < 8} { return 0 }
- set cmd_cooldown($chan) $now
- return 1
- }
- proc ::github::get_canonical_repo {input} {
- variable repo_map
- set lower [string tolower $input]
- return [expr {[info exists repo_map($lower)] ? $repo_map($lower) : $input}]
- }
- proc ::github::get_default_branch {repo} {
- set url "https://api.github.com/repos/$repo"
- lassign [::github::http_get $url] code body H
- if {$code != 200} {
- putlog "github: failed to get default branch for $repo (HTTP $code)"
- return "main"
- }
- set data [::json::json2dict $body]
- if {[dict exists $data default_branch]} {
- return [dict get $data default_branch]
- }
- return "main"
- }
- proc ::github::get_branch {repo} {
- variable repos
- if {[info exists repos($repo)]} {
- return [lindex $repos($repo) 0]
- }
- return [::github::get_default_branch $repo]
- }
- # USER COMMANDS
- proc ::github::cmd_latest {chan repo} {
- set branch [::github::get_branch $repo]
- set latest [::github::fetch_latest_commit $repo $branch]
- if {[llength $latest]} {
- array set C $latest
- set msg [::github::shortmsg $C(message) 100]
- putserv "PRIVMSG $chan :π§© Latest commit: $repo@$branch β $C(author): $msg β $C(url)"
- } else {
- putserv "PRIVMSG $chan :β Could not fetch latest commit for $repo (repo may not exist, be private, or rate-limited)"
- }
- set issues [::github::fetch_issues $repo 1]
- if {[llength $issues]} {
- array set I [lindex $issues 0]
- putserv "PRIVMSG $chan :π Latest open issue: #$I(number) by $I(user): $I(title) β $I(url)"
- }
- }
- proc ::github::cmd_commits {chan repo} {
- variable cfg
- set branch [::github::get_branch $repo]
- putserv "PRIVMSG $chan :π§© Last $cfg(cmd_limit) commits for $repo@$branch:"
- set url "https://api.github.com/repos/$repo/commits?per_page=$cfg(cmd_limit)&sha=$branch"
- lassign [::github::http_get $url] code body H
- if {$code < 200 || $code >= 300} {
- putserv "PRIVMSG $chan :β Error fetching commits (HTTP $code) β repo may not exist or be private"
- return
- }
- set data [::json::json2dict $body]
- if {![llength $data]} {
- putserv "PRIVMSG $chan :No commits found on branch $branch"
- return
- }
- foreach item $data {
- set commit [dict get $item commit]
- set msg [::github::shortmsg [dict get $commit message] 80]
- set url [dict get $item html_url]
- putserv "PRIVMSG $chan :β’ $msg β $url"
- }
- }
- proc ::github::cmd_issues {chan repo} {
- variable cfg
- putserv "PRIVMSG $chan :π Last $cfg(cmd_limit) open issues for $repo:"
- set issues [::github::fetch_issues $repo $cfg(cmd_limit)]
- if {![llength $issues]} {
- putserv "PRIVMSG $chan :No open issues found (repo may not exist, be private, or have none open)."
- return
- }
- foreach i $issues {
- array set I $i
- putserv "PRIVMSG $chan :β’ #$I(number): $I(title) β $I(url)"
- }
- }
- proc ::github::cmd_dispatch {nick host hand chan text} {
- variable default_repo
- if {![::github::cooldown_ok $chan]} return
- set args [split [string trim $text]]
- set sub [string tolower [lindex $args 0]]
- set input [lindex $args 1]
- if {$input eq ""} {
- set repo $default_repo
- } else {
- set repo [::github::get_canonical_repo $input]
- }
- switch -- $sub {
- latest { ::github::cmd_latest $chan $repo }
- commits { ::github::cmd_commits $chan $repo }
- issues { ::github::cmd_issues $chan $repo }
- default {
- putserv "PRIVMSG $chan :Usage: !gh <latest|commits|issues> \[owner/repo\]"
- putserv "PRIVMSG $chan :Monitored (auto-announce): [join [array names ::github::repos] ", "]"
- putserv "PRIVMSG $chan :Any public repo works for on-demand queries (e.g. !gh commits evilnet/x3)"
- }
- }
- }
- bind pub - "!gh" ::github::cmd_dispatch
- proc ::github::init {event} {
- ::github::load_state
- if {!$::github::cfg(announce_on_start)} {
- foreach repo [array names ::github::repos] {
- lassign $::github::repos($repo) branch channel
- set first [::github::fetch_latest_commit $repo $branch]
- if {[llength $first]} {
- array set C $first
- set ::github::state($repo,last_commit_sha) $C(sha)
- }
- set issues [::github::fetch_issues $repo 5]
- foreach i $issues {
- array set I $i
- lappend ::github::state($repo,seen_issue_ids) $I(id)
- }
- }
- ::github::save_state
- }
- putlog "github-multi: monitoring started"
- utimer 5 ::github::poll
- }
- bind evnt - init-server ::github::init