Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
d428ed892a | |||
0dd18f210d | |||
6a4892944b | |||
ef9886efc5 | |||
d4d49cbbad | |||
9fe5927465 | |||
8b1ad852f2 | |||
1b0c87f813 | |||
60b4af4139 | |||
1880916418 | |||
0309c953bd | |||
a719b892d4 | |||
|
fb1fc13329 | ||
|
3ace996a05 | ||
|
a98865c529 | ||
|
2848dea225 | ||
|
e17c83c621 | ||
|
b07d49671b | ||
|
a865fd7048 | ||
|
8d72e6dc2b | ||
|
1d8a1b2dea | ||
|
f9f4ed51db | ||
|
2db6df32fe | ||
|
def6d8133f |
@ -1 +1 @@
|
|||||||
SN_AUTH_COOKIE=
|
SN_API_KEY=
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
.env
|
.env*
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
unpaywall
|
nitterbot
|
||||||
|
2
Makefile
Normal file
2
Makefile
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
nitterbot: *.go go.sum go.mod
|
||||||
|
go build -o nitterbot .
|
@ -1,6 +1,6 @@
|
|||||||
# unpaywall
|
# nitterbot
|
||||||
|
|
||||||
This is a [Stacker News bot](https://stacker.news/unpaywall) that fetches every minute all recent posts.
|
This is a [Stacker News bot](https://stacker.news/nitter) that fetches every minute all recent posts.
|
||||||
|
|
||||||
If a post is a twitter link, it adds a comment with nitter links and stores the comment + item id in a sqlite3 database.
|
If a post is a twitter link, it adds a comment with nitter links and stores the comment + item id in a sqlite3 database.
|
||||||
|
|
||||||
|
10
db.go
10
db.go
@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/ekzyis/sn-goapi"
|
sn "github.com/ekzyis/snappy"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
db, err = sql.Open("sqlite3", "unpaywall.sqlite3")
|
db, err = sql.Open("sqlite3", "nitterbot.sqlite3")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -42,7 +42,9 @@ func ItemHasComment(parentId int) bool {
|
|||||||
err := db.QueryRow(`SELECT COUNT(1) FROM comments WHERE parent_id = ?`, parentId).Scan(&count)
|
err := db.QueryRow(`SELECT COUNT(1) FROM comments WHERE parent_id = ?`, parentId).Scan(&count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("error during item check: %w", err)
|
err = fmt.Errorf("error during item check: %w", err)
|
||||||
log.Fatal(err)
|
log.Println(err)
|
||||||
|
// pretend that item has comment to avoid duplicate comments
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
@ -51,6 +53,6 @@ func SaveComment(comment *sn.Comment) {
|
|||||||
_, err := db.Exec(`INSERT INTO comments(id, text, parent_id) VALUES (?, ?, ?)`, comment.Id, comment.Text, comment.ParentId)
|
_, err := db.Exec(`INSERT INTO comments(id, text, parent_id) VALUES (?, ?, ?)`, comment.Id, comment.Text, comment.ParentId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("error during item insert: %w", err)
|
err = fmt.Errorf("error during item insert: %w", err)
|
||||||
log.Fatal(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
6
go.mod
6
go.mod
@ -3,8 +3,6 @@ module git.ekzyis.com/ekzyis/unpaywall
|
|||||||
go 1.21.0
|
go 1.21.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ekzyis/sn-goapi v0.3.1 // indirect
|
github.com/ekzyis/snappy v0.5.3
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
|
||||||
github.com/namsral/flag v1.7.4-pre // indirect
|
|
||||||
)
|
)
|
||||||
|
18
go.sum
18
go.sum
@ -1,18 +1,4 @@
|
|||||||
github.com/ekzyis/sn-goapi v0.2.0 h1:k5YJlG+OguQEQRlbS5KghUttbfJaEkfvIfqB8B133FY=
|
github.com/ekzyis/snappy v0.5.3 h1:Pq8b2s4WPz5gnHgCoZR2LVqArEVqWeVCyHSVZCawat4=
|
||||||
github.com/ekzyis/sn-goapi v0.2.0/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
github.com/ekzyis/snappy v0.5.3/go.mod h1:BxJwdGlCwUw0Q5pQzBr59weAIS6pkVdivBBaZkkWTSo=
|
||||||
github.com/ekzyis/sn-goapi v0.2.1-0.20230823005215-5fb6668b6270 h1:aVDLDhrRsxP+if+ujCOpydFapeei2YL47Cp1ghgbJ2I=
|
|
||||||
github.com/ekzyis/sn-goapi v0.2.1-0.20230823005215-5fb6668b6270/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
|
||||||
github.com/ekzyis/sn-goapi v0.2.1-0.20230823015232-6a259372b331 h1:NpCmANlADOcRYxKe3GfaYcfra2fjTpW65M36Dq65LX8=
|
|
||||||
github.com/ekzyis/sn-goapi v0.2.1-0.20230823015232-6a259372b331/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.0 h1:pXRbD2iAMHvUC/+gZKXPRwInP4PuABEC1qQc+DWARlE=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.0/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.1-0.20230827213658-0ee21b998335 h1:Zl30HHHnM2LU0yXrrUUAGEGcSuoJXFcSt9ez0/tVPLI=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.1-0.20230827213658-0ee21b998335/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.1 h1:Cz/f7/yXMqnZNosRl4TRD3VHfxxkhKsKYCN7SwJc9ck=
|
|
||||||
github.com/ekzyis/sn-goapi v0.3.1/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
|
|
||||||
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
|
|
||||||
|
118
main.go
118
main.go
@ -1,18 +1,30 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ekzyis/sn-goapi"
|
sn "github.com/ekzyis/snappy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NostrClient struct {
|
||||||
|
Url string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
TwitterUrlRegexp = regexp.MustCompile(`^(?:https?:\/\/)?((www\.)?(twitter|x)\.com)\/`)
|
TwitterUrlRegexp = regexp.MustCompile(`(?:https?:\/\/)?(?:www\.)?((?:twitter|x)\.com)\/\w+\/status(?:es)?\/\d+`)
|
||||||
NitterUrls = []string{"nitter.net", "nitter.it", "nitter.cz"}
|
// references:
|
||||||
|
// - https://github.com/zedeus/nitter/wiki/Instances
|
||||||
|
// - https://status.d420.de/
|
||||||
|
NitterClearnetUrls = []string{
|
||||||
|
"xcancel.com",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func WaitUntilNext(d time.Duration) {
|
func WaitUntilNext(d time.Duration) {
|
||||||
@ -22,41 +34,17 @@ func WaitUntilNext(d time.Duration) {
|
|||||||
time.Sleep(dur)
|
time.Sleep(dur)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckNotifications() {
|
|
||||||
var prevHasNewNotes bool
|
|
||||||
for {
|
|
||||||
log.Println("Checking notifications ...")
|
|
||||||
hasNewNotes, err := sn.CheckNotifications()
|
|
||||||
if err != nil {
|
|
||||||
SendToNostr(fmt.Sprint(err))
|
|
||||||
} else {
|
|
||||||
if !prevHasNewNotes && hasNewNotes {
|
|
||||||
// only send on "rising edge"
|
|
||||||
SendToNostr("new notifications")
|
|
||||||
log.Println("Forwarded notifications to monitoring")
|
|
||||||
} else if hasNewNotes {
|
|
||||||
log.Println("Notifications already forwarded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prevHasNewNotes = hasNewNotes
|
|
||||||
WaitUntilNext(time.Hour)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SessionKeepAlive() {
|
|
||||||
for {
|
|
||||||
log.Println("Refresh session using GET /api/auth/session ...")
|
|
||||||
sn.RefreshSession()
|
|
||||||
WaitUntilNext(time.Hour)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
go CheckNotifications()
|
loadEnv()
|
||||||
go SessionKeepAlive()
|
|
||||||
|
c := sn.NewClient(
|
||||||
|
sn.WithBaseUrl(os.Getenv("SN_BASE_URL")),
|
||||||
|
sn.WithApiKey(os.Getenv("SN_API_KEY")),
|
||||||
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
log.Println("fetching items ...")
|
log.Println("fetching items ...")
|
||||||
r, err := sn.Items(&sn.ItemsQuery{Sort: "recent", Limit: 21})
|
r, err := c.Items(&sn.ItemsQuery{Sort: "recent", Type: "all", Limit: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
SendToNostr(fmt.Sprint(err))
|
SendToNostr(fmt.Sprint(err))
|
||||||
@ -65,32 +53,68 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range r.Items {
|
for _, item := range r.Items {
|
||||||
if m := TwitterUrlRegexp.FindStringSubmatch(item.Url); m != nil {
|
var m []string
|
||||||
|
var comment string
|
||||||
|
|
||||||
|
if m = TwitterUrlRegexp.FindStringSubmatch(item.Url); m != nil {
|
||||||
|
comment = strings.Replace(m[0], m[1], "xcancel.com", 1)
|
||||||
|
} else if m = TwitterUrlRegexp.FindStringSubmatch(item.Text); m != nil {
|
||||||
|
comment = strings.Replace(m[0], m[1], "xcancel.com", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if comment != "" {
|
||||||
log.Printf("item %d is twitter link\n", item.Id)
|
log.Printf("item %d is twitter link\n", item.Id)
|
||||||
|
|
||||||
if ItemHasComment(item.Id) {
|
if ItemHasComment(item.Id) {
|
||||||
log.Printf("item %d already has nitter links comment\n", item.Id)
|
log.Printf("item %d already has nitter links comment\n", item.Id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
comment := "Nitter links:\n\n"
|
|
||||||
for _, nUrl := range NitterUrls {
|
cId, err := c.CreateComment(item.Id, comment)
|
||||||
nitterLink := strings.Replace(item.Url, m[1], nUrl, 1)
|
|
||||||
comment += nitterLink + "\n"
|
|
||||||
}
|
|
||||||
comment += "\n_Nitter is a free and open source alternative Twitter front-end focused on privacy and performance. "
|
|
||||||
comment += "Click [here](https://github.com/zedeus/nitter) for more information._"
|
|
||||||
cId, err := sn.CreateComment(item.Id, comment)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println("create comment failed:", err)
|
||||||
SendToNostr(fmt.Sprint(err))
|
SendToNostr(fmt.Sprint(err))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("created comment %d\n", cId)
|
log.Printf("created comment %d\n", cId)
|
||||||
SaveComment(&sn.Comment{Id: cId, Text: comment, ParentId: item.Id})
|
SaveComment(&sn.Comment{Id: cId, Text: comment, ParentId: item.Id})
|
||||||
} else {
|
|
||||||
log.Printf("item %d is not twitter link\n", item.Id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WaitUntilNext(time.Minute)
|
WaitUntilNext(time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadEnv() {
|
||||||
|
var (
|
||||||
|
f *os.File
|
||||||
|
s *bufio.Scanner
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if f, err = os.Open(".env"); err != nil {
|
||||||
|
log.Fatalf("error opening .env: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
s = bufio.NewScanner(f)
|
||||||
|
s.Split(bufio.ScanLines)
|
||||||
|
for s.Scan() {
|
||||||
|
line := s.Text()
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
|
||||||
|
// Check if we have exactly 2 parts (key and value)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
os.Setenv(parts[0], parts[1])
|
||||||
|
} else {
|
||||||
|
log.Fatalf(".env: invalid line: %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors during scanning
|
||||||
|
if err = s.Err(); err != nil {
|
||||||
|
fmt.Println("error scanning .env:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user