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
* Welcome message of new members
* Limit of toots per hour
* Duplicate protection
* Logging
* Admin commands
### Admin commands
* unboost \<Toot ID>
* delete \<Toot ID>
* block \<User ID>
* unblock \<User ID>
# Configuration
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",
"AccessToken": "0000000000000000000000000000000000000000000",
"WelcomeMessage": "We have a new member in our group. Please love and favor"
"Max_toots": 1,
"Toots_interval": 24,
"Max_toots": 2,
"Toots_interval": 12,
"Duplicate_buf": 10,
"Admins": ["admin@example.com"]
}
```

43
bot.go
View file

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

View file

@ -9,8 +9,10 @@ import (
var (
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")
Conf = ReadConfig()
)
type Config struct {
@ -21,10 +23,11 @@ type Config struct {
WelcomeMessage string `json:"WelcomeMessage"`
Max_toots uint16 `json:"Max_toots"`
Toots_interval uint16 `json:"Toots_interval"`
Duplicate_buf int `json:"Duplicate_buf"`
Admins []string `json:"Admins"`
}
func ReadConf() Config {
func ReadConfig() Config {
flag.Parse()
data, err := os.ReadFile(*ConfPath)

View file

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

106
limits.go
View file

@ -15,25 +15,97 @@ func init_limit_db() *sql.DB {
if err != nil {
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 {
ErrorLogger.Println("Create database")
}
stat.Exec()
stat1.Exec()
stat2, err := db.Prepare(cmd2)
if err != nil {
ErrorLogger.Println("Create database")
}
stat2.Exec()
return db
}
// Add account to database
func add_to_db(acct string, limit uint16) {
func add_to_db(acct string) {
db := init_limit_db()
cmd := `INSERT INTO Limits (acct, ticket) VALUES (?, ?)`
stat, err := db.Prepare(cmd)
if err != nil {
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
@ -59,24 +131,8 @@ func take_ticket(acct string) {
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
func check_ticket(acct string, ticket uint16, toots_interval uint16) uint16 {
func check_ticket(acct string) uint16 {
db := init_limit_db()
cmd1 := `SELECT ticket 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)
since := time.Since(lastT)
limit := fmt.Sprintf("%dh", toots_interval)
limit := fmt.Sprintf("%dh", Conf.Toots_interval)
interval, _ := time.ParseDuration(limit)
if since >= interval {
@ -99,9 +155,9 @@ func check_ticket(acct string, ticket uint16, toots_interval uint16) uint16 {
if err != nil {
ErrorLogger.Println("Check ticket availability")
}
stat.Exec(ticket, acct)
stat.Exec(Conf.Max_toots, acct)
return ticket
return Conf.Max_toots
}
return tickets

View file

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

View file

@ -2,7 +2,7 @@
name=$RC_SVCNAME
command="/usr/bin/$name"
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"
pidfile="/run/$name.pid"
user="nobody"

View file

@ -6,7 +6,7 @@ Wants=network-online.target
[Service]
Type=simple
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]
WantedBy=multi-user.target