Compare commits

...

23 Commits

Author SHA1 Message Date
d428ed892a Remove 'item is not twitter link' log message 2024-09-08 21:50:30 +02:00
0dd18f210d Fix twitter regexp for comments by removing anchor 2024-09-08 21:47:32 +02:00
6a4892944b Also reply to comments with xcancel.com link 2024-09-08 21:05:18 +02:00
ef9886efc5 Add Makefile 2024-09-08 20:59:40 +02:00
d4d49cbbad Remove nostr parsing because SN has njump.me embeds now 2024-09-08 20:49:27 +02:00
9fe5927465 Fetch last 100 items 2024-09-08 20:43:27 +02:00
8b1ad852f2 Only log error, don't exit 2024-09-08 20:43:21 +02:00
1b0c87f813 Update .gitignore 2024-09-08 20:43:07 +02:00
60b4af4139 Fix coracle.social parsing 2024-07-03 09:40:22 +02:00
1880916418 Update to snappy v0.5.2 2024-07-03 09:40:22 +02:00
0309c953bd Use xcancel.com as nitter instance
Apparently, nitter is back via https://xcancel.com/
2024-07-03 08:42:42 +02:00
a719b892d4 Rename sqlite file 2024-07-03 08:42:10 +02:00
ekzyis
fb1fc13329 Fix nostr note parsing 2024-03-08 08:58:36 +01:00
ekzyis
3ace996a05 Fix missing client parsing 2024-02-26 16:06:15 +01:00
ekzyis
a98865c529 Parse nostr links 2024-02-26 15:43:35 +01:00
ekzyis
2848dea225 Update instance list 2024-02-24 16:05:00 +01:00
ekzyis
e17c83c621 Update to sn-goapi v0.3.3 2024-02-04 15:58:57 +01:00
ekzyis
b07d49671b Update clearnet instances 2024-02-04 15:54:18 +01:00
ekzyis
a865fd7048 Update README 2024-02-04 15:13:55 +01:00
ekzyis
8d72e6dc2b Update to sn-goapi v0.3.2 2024-02-04 15:13:55 +01:00
ekzyis
1d8a1b2dea Make layout even smaller 2024-02-04 15:13:55 +01:00
ekzyis
f9f4ed51db Make layout smaller 2024-02-04 15:13:55 +01:00
ekzyis
2db6df32fe Add more instances + Tor, I2P, Lokinet
* add more clearnet instances
* add TOR + I2P + Lokinet instances
* change layout + heading
2024-02-04 15:13:55 +01:00
8 changed files with 87 additions and 68 deletions

View File

@ -1 +1 @@
SN_AUTH_COOKIE= SN_API_KEY=

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
.env .env*
*.sqlite3 *.sqlite3
unpaywall nitterbot

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
nitterbot: *.go go.sum go.mod
go build -o nitterbot .

View File

@ -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
View File

@ -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)
} }
} }

7
go.mod
View File

@ -3,11 +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 github.com/ekzyis/snappy v0.5.3
github.com/mattn/go-sqlite3 v1.14.17 github.com/mattn/go-sqlite3 v1.14.17
) )
require (
github.com/joho/godotenv v1.5.1 // indirect
github.com/namsral/flag v1.7.4-pre // indirect
)

8
go.sum
View File

@ -1,8 +1,4 @@
github.com/ekzyis/sn-goapi v0.3.1 h1:IAlbqksX6Vjj25aDFZxml0pAmHgqWDsK3gTYEiIKCcs= github.com/ekzyis/snappy v0.5.3 h1:Pq8b2s4WPz5gnHgCoZR2LVqArEVqWeVCyHSVZCawat4=
github.com/ekzyis/sn-goapi v0.3.1/go.mod h1:FObbYr/NXgnXNWU+EwiWKoWQy+wAaRS6AoW3NgsJ/Oo= github.com/ekzyis/snappy v0.5.3/go.mod h1:BxJwdGlCwUw0Q5pQzBr59weAIS6pkVdivBBaZkkWTSo=
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
View File

@ -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)
}
}