diff --git a/plugin.json b/plugin.json index dd28326..1c66364 100644 --- a/plugin.json +++ b/plugin.json @@ -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" }, { diff --git a/server/configuration.go b/server/configuration.go index 71e2bb2..a029b38 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -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 diff --git a/server/plugin.go b/server/plugin.go index 9cc42c1..5ddb600 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -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) } @@ -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) { @@ -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, "" +}