Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,33 @@
"key": "BlockNewUserPMTime",
"display_name": "Block New User PMs Time:",
"type": "text",
"help_text": "How long to block PMs for (duration (e.g., 24h, or 12h30m))",
"help_text": "How long to block PMs for (duration (e.g., 24h, or 12h30m)). Use -1 to enable the filter indefinitely.",
"default": "24h"
},
{
"key": "BlockNewUserLinks",
"display_name": "Block New User Links:",
"type": "bool",
"help_text": "Configure whether to block new users from posting links for some time (see BlockNewUserLinksTime)"
},
{
"key": "BlockNewUserLinksTime",
"display_name": "Block New User Links Time:",
"type": "text",
"help_text": "How long to block link posts for (duration (e.g., 24h, or 12h30m)). Use -1 to enable the filter indefinitely.",
"default": "24h"
},
{
"key": "BlockNewUserImages",
"display_name": "Block New User Images:",
"type": "bool",
"help_text": "Configure whether to block new users from posting images for some time (see BlockNewUserImagesTime)"
},
{
"key": "BlockNewUserImagesTime",
"display_name": "Block New User Images Time:",
"type": "text",
"help_text": "How long to block image posts for (duration (e.g., 24h, or 12h30m)). Use -1 to enable the filter indefinitely.",
"default": "24h"
},
{
Expand Down
24 changes: 14 additions & 10 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ import (
// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep
// copy appropriate for your types.
type configuration struct {
BadDomainsList string
BadUsernamesList string
BuiltinBadDomains bool
BadWordsList string
BlockNewUserPM bool
BlockNewUserPMTime string
CensorCharacter string
ExcludeBots bool
RejectPosts bool
WarningMessage string `json:"WarningMessage"`
BadDomainsList string
BadUsernamesList string
BuiltinBadDomains bool
BadWordsList string
BlockNewUserPM bool
BlockNewUserPMTime string
BlockNewUserLinks bool
BlockNewUserLinksTime string
BlockNewUserImages bool
BlockNewUserImagesTime string
CensorCharacter string
ExcludeBots bool
RejectPosts bool
WarningMessage string `json:"WarningMessage"`
}

//go:embed bad-domains.txt
Expand Down
155 changes: 134 additions & 21 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ func (p *Plugin) FilterPost(post *model.Post) (*model.Post, string) {
return p.FilterDirectMessage(configuration, post)
}

if configuration.BlockNewUserLinks && p.containsLinks(post) {
return p.FilterNewUserLinks(configuration, post)
}

if configuration.BlockNewUserImages && p.containsImages(post) {
return p.FilterNewUserImages(configuration, post)
}

return p.FilterPostBadWords(configuration, post)
}

Expand All @@ -70,27 +78,12 @@ func (p *Plugin) GetUserByID(userID string) (*model.User, error) {
}

func (p *Plugin) FilterDirectMessage(configuration *configuration, post *model.Post) (*model.Post, string) {
user, err := p.GetUserByID(post.UserId)
if err != nil {
p.sendUserEphemeralMessageForPost(post, "Something went wrong when sending your message. Contact an administrator.")
return nil, "Failed to get user"
}

userCreateSeconds := user.CreateAt / 1000
createdAt := time.Unix(userCreateSeconds, 0)
blockDuration := configuration.BlockNewUserPMTime
duration, parseErr := time.ParseDuration(blockDuration)

if parseErr != nil {
p.sendUserEphemeralMessageForPost(post, "Something went wrong when sending your message. Contact an administrator.")
return nil, "failed to parse duration"
}

if time.Since(createdAt) < duration {
p.sendUserEphemeralMessageForPost(post, "Configuration settings limit new users from sending private messages.")
return nil, fmt.Sprintf("New user not allowed to send DM for %s.", duration)
}
return post, ""
return p.filterNewUserContent(
post,
"direct messages",
configuration.BlockNewUserPMTime,
"Configuration settings limit new users from sending private messages.",
)
}

func (p *Plugin) FilterPostBadWords(configuration *configuration, post *model.Post) (*model.Post, string) {
Expand Down Expand Up @@ -209,3 +202,123 @@ func (p *Plugin) cleanupUser(user *model.User) bool {

return true
}

// FilterNewUserLinks checks if a new user is trying to post links and blocks them if they're too new
func (p *Plugin) FilterNewUserLinks(configuration *configuration, post *model.Post) (*model.Post, string) {
return p.filterNewUserContent(
post,
"links",
configuration.BlockNewUserLinksTime,
"Configuration settings limit new users from posting links.",
)
}

// FilterNewUserImages checks if a new user is trying to post images and blocks them if they're too new
func (p *Plugin) FilterNewUserImages(configuration *configuration, post *model.Post) (*model.Post, string) {
return p.filterNewUserContent(
post,
"images",
configuration.BlockNewUserImagesTime,
"Configuration settings limit new users from posting images.",
)
}

// containsLinks checks if a post contains links
func (p *Plugin) containsLinks(post *model.Post) bool {
// Check if the post has embeds (which includes OpenGraph metadata for links)
if post.Metadata != nil && len(post.Metadata.Embeds) > 0 {
return true
}

// Check if the post message contains URLs
// This is a simple regex to detect URLs in the message
urlRegex := regexp.MustCompile(`https?://[^\s<>"]+|www\.[^\s<>"]+`)
return urlRegex.MatchString(post.Message)
}

// containsImages checks if a post contains images
func (p *Plugin) containsImages(post *model.Post) bool {
// Check if the post has file attachments that are images
if post.Metadata != nil && len(post.Metadata.Files) > 0 {
for _, file := range post.Metadata.Files {
// Check if the file is an image based on its extension
if strings.HasPrefix(file.Extension, ".") {
ext := strings.ToLower(file.Extension)
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".bmp" || ext == ".webp" {
return true
}
}
}
}

// Check if the post has image embeds
if post.Metadata != nil && len(post.Metadata.Images) > 0 {
return true
}

// Check if the post message contains Markdown image syntax
imageRegex := regexp.MustCompile(`!\[.*?\]\(.*?\)`)
return imageRegex.MatchString(post.Message)
}

// isUserTooNew checks if a user is too new based on the configured duration
// Returns (isTooNew, errorMessage, error)
func (p *Plugin) isUserTooNew(user *model.User, blockDuration string, contentType string) (bool, string, error) {
// Check if the filter is enabled indefinitely (duration is -1)
if blockDuration == "-1" {
return true, fmt.Sprintf("New user not allowed to post %s indefinitely.", contentType), nil
}

userCreateSeconds := user.CreateAt / 1000
createdAt := time.Unix(userCreateSeconds, 0)
duration, parseErr := time.ParseDuration(blockDuration)

if parseErr != nil {
return false, "", fmt.Errorf("failed to parse duration: %w", parseErr)
}

if time.Since(createdAt) < duration {
return true, fmt.Sprintf("New user not allowed to post %s for %s.", contentType, duration), nil
}

return false, "", nil
}

// getUserAndHandleError retrieves a user by ID and handles any errors
func (p *Plugin) getUserAndHandleError(userID string, post *model.Post) (*model.User, string) {
user, err := p.GetUserByID(userID)
if err != nil {
p.sendUserEphemeralMessageForPost(post, "Something went wrong when sending your message. Contact an administrator.")
return nil, "Failed to get user"
}
return user, ""
}

// handleFilterError handles errors from the isUserTooNew function
func (p *Plugin) handleFilterError(err error, post *model.Post) (*model.Post, string) {
if err != nil {
p.sendUserEphemeralMessageForPost(post, "Something went wrong when sending your message. Contact an administrator.")
return nil, err.Error()
}
return nil, ""
}

// filterNewUserContent is a generic function to filter content from new users
func (p *Plugin) filterNewUserContent(post *model.Post, contentType string, blockDuration string, userMessage string) (*model.Post, string) {
user, errMsg := p.getUserAndHandleError(post.UserId, post)
if errMsg != "" {
return nil, errMsg
}

isTooNew, errorMsg, err := p.isUserTooNew(user, blockDuration, contentType)
if err != nil {
return p.handleFilterError(err, post)
}

if isTooNew {
p.sendUserEphemeralMessageForPost(post, userMessage)
return nil, errorMsg
}

return post, ""
}