add duplicate protection

This commit is contained in:
fade 2022-09-01 07:31:27 -04:00
parent 373f3721dd
commit 84bb8e3154
8 changed files with 126 additions and 52 deletions

View File

@ -5,13 +5,13 @@ This is a bot which implements group functionality in Mastodon.
* Repost toots * Repost toots
* Welcome message of new members * Welcome message of new members
* Limit of toots per hour * Limit of toots per hour
* Duplicate protection
* Logging
* Admin commands * Admin commands
### Admin commands ### Admin commands
* unboost \<Toot ID> * unboost \<Toot ID>
* delete \<Toot ID> * delete \<Toot ID>
* block \<User ID>
* unblock \<User ID>
# Configuration # Configuration
The bot is configured in a JSON file that looks like this: The bot is configured in a JSON file that looks like this:
@ -22,8 +22,9 @@ The bot is configured in a JSON file that looks like this:
"ClientSecret": "0000000000000000000000000000000000000000000", "ClientSecret": "0000000000000000000000000000000000000000000",
"AccessToken": "0000000000000000000000000000000000000000000", "AccessToken": "0000000000000000000000000000000000000000000",
"WelcomeMessage": "We have a new member in our group. Please love and favor" "WelcomeMessage": "We have a new member in our group. Please love and favor"
"Max_toots": 1, "Max_toots": 2,
"Toots_interval": 24, "Toots_interval": 12,
"Duplicate_buf": 10,
"Admins": ["admin@example.com"] "Admins": ["admin@example.com"]
} }
``` ```

43
bot.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"crypto/sha512"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -9,7 +10,7 @@ import (
"github.com/mattn/go-mastodon" "github.com/mattn/go-mastodon"
) )
func RunBot(Conf Config) { func RunBot() {
logger_init() logger_init()
c := mastodon.NewClient(&mastodon.Config{ c := mastodon.NewClient(&mastodon.Config{
@ -59,7 +60,7 @@ func RunBot(Conf Config) {
if !followed(acct) { // Add to db and post welcome message if !followed(acct) { // Add to db and post welcome message
InfoLogger.Printf("%s followed", acct) InfoLogger.Printf("%s followed", acct)
add_to_db(acct, Conf.Max_toots) add_to_db(acct)
InfoLogger.Printf("%s added to database", acct) InfoLogger.Printf("%s added to database", acct)
var message = fmt.Sprintf("%s @%s", Conf.WelcomeMessage, acct) var message = fmt.Sprintf("%s @%s", Conf.WelcomeMessage, acct)
@ -74,31 +75,49 @@ func RunBot(Conf Config) {
// Read message // Read message
if notif.Type == "mention" { if notif.Type == "mention" {
acct := notif.Status.Account.Acct acct := notif.Status.Account.Acct
content := notif.Status.Content
tooturl := notif.Status.URL
for i := 0; i < len(followers); i++ { for i := 0; i < len(followers); i++ {
if acct == string(followers[i].Acct) { // Follow check if acct == string(followers[i].Acct) { // Follow check
if notif.Status.Visibility == "public" { // Reblog toot if notif.Status.Visibility == "public" { // Reblog toot
if notif.Status.InReplyToID == nil { // Not boost replies if notif.Status.InReplyToID == nil { // Not boost replies
if !followed(acct) { // Add to db if needed // Duplicate protection
add_to_db(acct, Conf.Max_toots) content_hash := sha512.New()
content_hash.Write([]byte(content))
hash := fmt.Sprintf("%x", content_hash.Sum(nil))
if !check_msg_hash(hash) {
save_msg_hash(hash)
InfoLogger.Printf("Hash of %s added to database", tooturl)
} else {
WarnLogger.Printf("%s is a duplicate and not boosted", tooturl)
break
}
// Add to db if needed
if !followed(acct) {
add_to_db(acct)
InfoLogger.Printf("%s added to database", acct) InfoLogger.Printf("%s added to database", acct)
} }
if check_ticket(acct, Conf.Max_toots, Conf.Toots_interval) > 0 { // Limit
// Message limit
if check_ticket(acct) > 0 {
take_ticket(acct) take_ticket(acct)
InfoLogger.Printf("Ticket of %s was taken", acct) InfoLogger.Printf("Ticket of %s was taken", acct)
c.Reblog(ctx, notif.Status.ID) c.Reblog(ctx, notif.Status.ID)
InfoLogger.Printf("Toot %s of %s was rebloged", notif.Status.URL, acct) InfoLogger.Printf("Toot %s of %s was rebloged", tooturl, acct)
} else { } else {
WarnLogger.Printf("%s haven't tickets", acct) WarnLogger.Printf("%s haven't tickets", acct)
} }
} else { } else {
WarnLogger.Printf("%s is reply and not boosted", notif.Status.URL) WarnLogger.Printf("%s is reply and not boosted", tooturl)
} }
} else if notif.Status.Visibility == "direct" { // Admin commands } else if notif.Status.Visibility == "direct" { // Admin commands
for y := 0; y < len(Conf.Admins); y++ { for y := 0; y < len(Conf.Admins); y++ {
if acct == Conf.Admins[y] { if acct == Conf.Admins[y] {
text := notif.Status.Content
recmd := regexp.MustCompile(`<.*?> `) recmd := regexp.MustCompile(`<.*?> `)
command := recmd.ReplaceAllString(text, "") command := recmd.ReplaceAllString(content, "")
args := strings.Split(command, " ") args := strings.Split(command, " ")
mID := mastodon.ID((args[1])) mID := mastodon.ID((args[1]))
@ -108,10 +127,6 @@ func RunBot(Conf Config) {
c.Unreblog(ctx, mID) c.Unreblog(ctx, mID)
case "delete": case "delete":
c.DeleteStatus(ctx, mID) c.DeleteStatus(ctx, mID)
case "block":
c.AccountBlock(ctx, mID)
case "unblock":
c.AccountUnblock(ctx, mID)
} }
} }
} else { } else {
@ -119,7 +134,7 @@ func RunBot(Conf Config) {
} }
} }
} else { } else {
WarnLogger.Printf("%s is not public toot and not boosted", notif.Status.URL) WarnLogger.Printf("%s is not public toot and not boosted", tooturl)
break break
} }
} }

View File

@ -9,8 +9,10 @@ import (
var ( var (
ConfPath = flag.String("config", "config.json", "Path to config") ConfPath = flag.String("config", "config.json", "Path to config")
DBPath = flag.String("db", "limits.db", "Path to database") DBPath = flag.String("db", "mastodon-group-bot.db", "Path to database")
LogPath = flag.String("log", "mastodon-group-bot.log", "Path to log") LogPath = flag.String("log", "mastodon-group-bot.log", "Path to log")
Conf = ReadConfig()
) )
type Config struct { type Config struct {
@ -21,10 +23,11 @@ type Config struct {
WelcomeMessage string `json:"WelcomeMessage"` WelcomeMessage string `json:"WelcomeMessage"`
Max_toots uint16 `json:"Max_toots"` Max_toots uint16 `json:"Max_toots"`
Toots_interval uint16 `json:"Toots_interval"` Toots_interval uint16 `json:"Toots_interval"`
Duplicate_buf int `json:"Duplicate_buf"`
Admins []string `json:"Admins"` Admins []string `json:"Admins"`
} }
func ReadConf() Config { func ReadConfig() Config {
flag.Parse() flag.Parse()
data, err := os.ReadFile(*ConfPath) data, err := os.ReadFile(*ConfPath)

View File

@ -4,7 +4,8 @@
"ClientSecret": "0000000000000000000000000000000000000000000", "ClientSecret": "0000000000000000000000000000000000000000000",
"AccessToken": "0000000000000000000000000000000000000000000", "AccessToken": "0000000000000000000000000000000000000000000",
"WelcomeMessage": "We have a new member in our group. Please love and favor", "WelcomeMessage": "We have a new member in our group. Please love and favor",
"Max_toots": 1, "Max_toots": 2,
"Toots_interval": 24, "Toots_interval": 12,
"Duplicate_buf": 10,
"Admins": ["admin@example.com"] "Admins": ["admin@example.com"]
} }

106
limits.go
View File

@ -15,25 +15,97 @@ func init_limit_db() *sql.DB {
if err != nil { if err != nil {
ErrorLogger.Println("Open database") ErrorLogger.Println("Open database")
} }
cmd := `CREATE TABLE IF NOT EXISTS Limits (id INTEGER PRIMARY KEY AUTOINCREMENT, acct TEXT, ticket INTEGER, time TEXT)`
stat, err := db.Prepare(cmd) cmd1 := `CREATE TABLE IF NOT EXISTS Limits (id INTEGER PRIMARY KEY AUTOINCREMENT, acct TEXT, ticket INTEGER, time TEXT)`
cmd2 := `CREATE TABLE IF NOT EXISTS MsgHashs (message_hash TEXT)`
stat1, err := db.Prepare(cmd1)
if err != nil { if err != nil {
ErrorLogger.Println("Create database") ErrorLogger.Println("Create database")
} }
stat.Exec() stat1.Exec()
stat2, err := db.Prepare(cmd2)
if err != nil {
ErrorLogger.Println("Create database")
}
stat2.Exec()
return db return db
} }
// Add account to database // Add account to database
func add_to_db(acct string, limit uint16) { func add_to_db(acct string) {
db := init_limit_db() db := init_limit_db()
cmd := `INSERT INTO Limits (acct, ticket) VALUES (?, ?)` cmd := `INSERT INTO Limits (acct, ticket) VALUES (?, ?)`
stat, err := db.Prepare(cmd) stat, err := db.Prepare(cmd)
if err != nil { if err != nil {
ErrorLogger.Println("Add account to databse") ErrorLogger.Println("Add account to databse")
} }
stat.Exec(acct, limit) stat.Exec(acct, Conf.Max_toots)
}
// Save message hash
func save_msg_hash(hash string) {
db := init_limit_db()
cmd1 := `SELECT COUNT(*) FROM MsgHashs`
cmd2 := `DELETE FROM MsgHashs WHERE ROWID IN (SELECT ROWID FROM MsgHashs LIMIT 1)`
cmd3 := `INSERT INTO MsgHashs (message_hash) VALUES (?)`
var rows int
db.QueryRow(cmd1).Scan(&rows)
if rows >= Conf.Duplicate_buf {
superfluous := rows - Conf.Duplicate_buf
for i := 0; i <= superfluous; i++ {
stat2, err := db.Prepare(cmd2)
if err != nil {
ErrorLogger.Println("Delete message hash from database")
}
stat2.Exec()
}
}
stat1, err := db.Prepare(cmd3)
if err != nil {
ErrorLogger.Println("Add message hash to database")
}
stat1.Exec(hash)
}
// Check followed once
func followed(acct string) bool {
db := init_limit_db()
cmd := `SELECT acct FROM Limits WHERE acct = ?`
err := db.QueryRow(cmd, acct).Scan(&acct)
if err != nil {
if err != sql.ErrNoRows {
InfoLogger.Println("Check followed")
}
return false
}
return true
}
// Check message hash
func check_msg_hash(hash string) bool {
db := init_limit_db()
cmd := `SELECT message_hash FROM MsgHashs WHERE message_hash = ?`
err := db.QueryRow(cmd, hash).Scan(&hash)
if err != nil {
if err != sql.ErrNoRows {
InfoLogger.Println("Check message hash in database")
}
return false
}
return true
} }
// Take ticket for tooting // Take ticket for tooting
@ -59,24 +131,8 @@ func take_ticket(acct string) {
stat.Exec(ticket, last_toot_at, acct) stat.Exec(ticket, last_toot_at, acct)
} }
// Check followed once
func followed(acct string) bool {
db := init_limit_db()
cmd := `SELECT acct FROM Limits WHERE acct = ?`
err := db.QueryRow(cmd, acct).Scan(&acct)
if err != nil {
if err != sql.ErrNoRows {
ErrorLogger.Println("Check followed")
}
return false
}
return true
}
// Check ticket availability // Check ticket availability
func check_ticket(acct string, ticket uint16, toots_interval uint16) uint16 { func check_ticket(acct string) uint16 {
db := init_limit_db() db := init_limit_db()
cmd1 := `SELECT ticket FROM Limits WHERE acct = ?` cmd1 := `SELECT ticket FROM Limits WHERE acct = ?`
cmd2 := `SELECT time FROM Limits WHERE acct = ?` cmd2 := `SELECT time FROM Limits WHERE acct = ?`
@ -90,7 +146,7 @@ func check_ticket(acct string, ticket uint16, toots_interval uint16) uint16 {
lastT, _ := time.Parse("2006/01/02 15:04:05 MST", lastS) lastT, _ := time.Parse("2006/01/02 15:04:05 MST", lastS)
since := time.Since(lastT) since := time.Since(lastT)
limit := fmt.Sprintf("%dh", toots_interval) limit := fmt.Sprintf("%dh", Conf.Toots_interval)
interval, _ := time.ParseDuration(limit) interval, _ := time.ParseDuration(limit)
if since >= interval { if since >= interval {
@ -99,9 +155,9 @@ func check_ticket(acct string, ticket uint16, toots_interval uint16) uint16 {
if err != nil { if err != nil {
ErrorLogger.Println("Check ticket availability") ErrorLogger.Println("Check ticket availability")
} }
stat.Exec(ticket, acct) stat.Exec(Conf.Max_toots, acct)
return ticket return Conf.Max_toots
} }
return tickets return tickets

View File

@ -1,7 +1,5 @@
package main package main
func main() { func main() {
config := ReadConf() RunBot()
RunBot(config)
} }

View File

@ -2,7 +2,7 @@
name=$RC_SVCNAME name=$RC_SVCNAME
command="/usr/bin/$name" command="/usr/bin/$name"
command_arg1="-config /etc/$name/config.json" command_arg1="-config /etc/$name/config.json"
command_arg2="-db /var/lib/$name/limits.db" command_arg2="-db /var/lib/$name/$name.db"
command_arg3="-log /var/log/$name/$name.log" command_arg3="-log /var/log/$name/$name.log"
pidfile="/run/$name.pid" pidfile="/run/$name.pid"
user="nobody" user="nobody"

View File

@ -6,7 +6,7 @@ Wants=network-online.target
[Service] [Service]
Type=simple Type=simple
User=nobody User=nobody
ExecStart=/usr/bin/mastodon-group-bot -config /etc/mastodon-group-bot/config.json -db /var/lib/mastodon-group-bot/limits.db -log /var/log/mastodon-group-bot/mastodon-group-bot.log ExecStart=/usr/bin/mastodon-group-bot -config /etc/mastodon-group-bot/config.json -db /var/lib/mastodon-group-bot/mastodon-group-bot.db -log /var/log/mastodon-group-bot/mastodon-group-bot.log
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target