mirror of
https://gitea.phreedom.club/localhost_frssoft/mastodon-group-bot
synced 2024-11-24 05:21:28 +00:00
add duplicate protection
This commit is contained in:
parent
373f3721dd
commit
84bb8e3154
|
@ -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
43
bot.go
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
106
limits.go
|
@ -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
|
||||||
|
|
4
main.go
4
main.go
|
@ -1,7 +1,5 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := ReadConf()
|
RunBot()
|
||||||
|
|
||||||
RunBot(config)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
Loading…
Reference in a new issue