M config.example.ini => config.example.ini +1 -2
@@ 1,7 1,6 @@
[database]
path = posts.db
-max_posts = 10000
[atproto]
jetstream_host = jetstream2.us-east.bsky.network
-publisher_did = ; your atproto did
+; publisher_did = <your atproto did>
M main.tcl => main.tcl +25 -2
@@ 1,4 1,7 @@
#!/bin/env tclsh
+
+cd [file dirname [dict get [info frame [info frame]] file]]
+
package require logger
package require json
package require json::write
@@ 17,6 20,7 @@ source src/feed.tcl
source src/algos.tcl
set log [logger::init tclfeed]
+set logd [logger::init tclfeed::database]
${log}::info "tclfeed v0.0.1"
### DB STUFF ###
@@ 35,14 39,33 @@ foreach algo $::algos::list {
}
}
+proc db_cleanup {interval max_posts} {
+ global ::algos::list logd
+
+ ${logd}::info "Performing database cleanup!"
+
+ foreach algo $::algos::list {
+ # slightly cursed
+ db eval "DELETE FROM $algo WHERE uri IN (SELECT uri FROM $algo ORDER BY ord DESC LIMIT -1 OFFSET :max_posts); REINDEX $algo;"
+ }
+
+ db eval "VACUUM";
+
+ ${logd}::info "DB cleanup completed without error!"
+ ${logd}::debug "Scheduling next cleanup for [clock format [expr {[clock seconds] + $interval}]]."
+
+ after [expr {$interval * 1000}] [list db_cleanup $interval $max_posts]
+}
+db_cleanup [::ini::value $config database cleanup_interval] [::ini::value $config database max_posts]
+
### JETSTREAM ###
jetstream::listen [::ini::value $config atproto jetstream_host] $::algos::list
### HTTPD ###
httpd::router main [list \
- /xrpc/app.bsky.feed.getFeedSkeleton [list ::feed::serve_skeleton $::algos::list] \
+ /xrpc/app.bsky.feed.getFeedSkeleton [list ::feed::serve_skeleton $::algos::list [::ini::value $config atproto publisher_did]] \
]
-httpd::listen main 3000
+httpd::listen main [::ini::value $config httpd port]
vwait nil
M src/algos/tclposting.tcl => src/algos/tclposting.tcl +26 -3
@@ 2,9 2,32 @@ namespace eval ::algos::tclposting {
variable log [logger::init algos::tclposting]
proc intake {uri text data} {
variable log
- if {[regexp -- {\s[Tt]cl(/[Tt]k)?(\.|,|\s)|^[Tt]cl(/[Tt]k)?(,|\s)|\s[Tt]cl(/[Tt]k)?$|\sTk(\.|,|\s)|^Tk(,|\s)|\sTk$|\.tcl|tclsh|tcl-lang|tcltk|tcllib|tklib|tcl\.tk|#tcllang} $text]} {
- return [dict get $data commit record createdAt]
+ if {![regexp -- [join {
+ {\s[Tt]cl(/[Tt]k)?(\.|,|\s)}
+ {^[Tt]cl(/[Tt]k)?(,|\s)}
+ {\s[Tt]cl(/[Tt]k)?$}
+ {\sTk(\.|,|\s)}
+ {^Tk(,|\s)}
+ {\sTk$}
+ {\.tcl}
+ {tclsh}
+ {tcl-lang}
+ {tcltk}
+ {tcllib}
+ {tklib}
+ {tcl\.tk}
+ {#tcllang}} | ] $text]} \
+ {
+ return {}
}
- return {}
+
+ if {[regexp -nocase -- [join {
+ {Title Tk}
+ {Comic-Con Tk}} | ] $text]} \
+ {
+ return {}
+ }
+
+ return [dict get $data commit record createdAt]
}
}
M src/config.tcl => src/config.tcl +22 -3
@@ 2,23 2,42 @@ if {$argc != 1} {
puts stderr "Usage: $argv0 </path/to/config/file.ini>"
exit -1
}
-if [catch {set config [::ini::open [lindex $argv 0]]} result] {
+if [catch {set config [::ini::open [lindex $argv 0] r]} result] {
puts stderr "Couldn't open config: $result"
exit -1
}
namespace eval ::config {
+ variable log [logger::init "tclfeed::config"]
proc require {config section key} {
- if ![ini::exists $config $section $key] {
+ variable log
+ if ![::ini::exists $config $section $key] {
puts stderr "Missing [$section/$key] in config!"
exit -2
}
+ ${log}::info "\[$section/$key\] = [::ini::value $config $section $key]"
+ }
+ proc optional {config section key value} {
+ variable log
+ if ![::ini::exists $config $section $key] {
+ ::ini::set $config $section $key $value
+ ${log}::info "\[$section/$key\] = [::ini::value $config $section $key] (default)"
+ } else {
+ ${log}::info "\[$section/$key\] = [::ini::value $config $section $key]"
+ }
}
proc check {config} {
+ variable log
+ ${log}::info "Loading config [::ini::filename $config]..."
require $config database path
- require $config database max_posts
+ optional $config database max_posts 10000
+ optional $config database cleanup_interval 600
require $config atproto jetstream_host
+ require $config atproto publisher_did
+
+ optional $config httpd port 3000
+ ${log}::info "Config loaded!..."
}
}
M src/feed.tcl => src/feed.tcl +11 -9
@@ 1,7 1,5 @@
namespace eval feed {
- proc ::feed::serve_skeleton {feeds _ sock path query headers} {
- global config
-
+ proc ::feed::serve_skeleton {feeds publisher _ sock path query headers} {
if ![dict exists $query feed] {
puts $sock "HTTP/1.0 400 Bad Request"
puts $sock "Content-Type: text/plain"
@@ 20,8 18,8 @@ namespace eval feed {
if [catch {
set feed [dict get $query feed]
- set cursor [expr {min(0, [dict get $query cursor])}]
- set limit [expr {min(1, max(100, [dict get $query limit]))}]
+ set cursor [expr {max(0, [dict get $query cursor])}]
+ set limit [expr {max(1, min(100, [dict get $query limit]))}]
}] {
puts $sock "HTTP/1.0 400 Bad Request"
puts $sock "Content-Type: text/plain"
@@ 35,7 33,7 @@ namespace eval feed {
set feed [::at::uri $feed]
if {[llength $feed] != 6 ||
- [dict get $feed authority] != [::ini::value $config atproto publisher_did] ||
+ [dict get $feed authority] != $publisher ||
[dict get $feed collection] != "app.bsky.feed.generator"} {
puts $sock "HTTP/1.0 400 Bad Request"
puts $sock "Content-Type: text/plain"
@@ 59,11 57,11 @@ namespace eval feed {
}
set feed_data {}
- foreach uri [db eval "SELECT uri FROM $feed ORDER BY ord LIMIT $limit OFFSET $cursor;"] {
+ foreach uri [db eval "SELECT uri FROM $feed ORDER BY ord DESC LIMIT $limit OFFSET $cursor;"] {
lappend feed_data [::json::write object post [::json::write string $uri]]
}
- set feed_data [::json::write array {*}$feed_data]
- set feed_contents [::json::write object cursor [expr {$cursor + $limit}] feed $feed_data]
+ set feed_data_arr [::json::write array {*}$feed_data]
+ set feed_contents [::json::write object cursor [expr {$cursor + min([llength $feed_data], $limit)}] feed $feed_data_arr]
puts $sock "HTTP/1.0 200 OK"
puts $sock "Content-Type: application/json"
@@ 74,4 72,8 @@ namespace eval feed {
close $sock
return
}
+
+ proc ::feed::serve_generator_description {feeds publisher _ sock path query headers} {
+
+ }
}