Skip to content
Merged
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
11 changes: 10 additions & 1 deletion activitylog/activitylog.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,14 @@ def update_cache(

def should_log(
self,
location: Union[discord.Guild, discord.abc.GuildChannel, discord.DMChannel, discord.Thread, discord.User],
location: Union[
discord.Guild,
discord.ForumChannel,
discord.abc.GuildChannel,
discord.DMChannel,
discord.Thread,
discord.User,
],
) -> bool:
if not self.cache or not self.is_initalized:
# cache is empty, still booting
Expand All @@ -277,6 +284,8 @@ def should_log(
return loc.get("all_s", False) or loc.get("events", default)

elif type(location) is discord.TextChannel or type(location) is discord.Thread:
if type(location) == discord.Thread:
location = location.parent
loc = self.cache[location.guild.id]
opts = [
loc.get("all_s", False),
Expand Down
210 changes: 210 additions & 0 deletions campaigns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Campaign Cog Documentation

## Overview

The Campaign Cog is a comprehensive Red-Discordbot extension designed to facilitate user feedback collection through customizable campaigns. This cog enables server administrators to create structured surveys with up to 20 questions, distribute them to users, and export the collected data for analysis.

## Features

### Core Functionality
- **Campaign Creation**: Create named campaigns with descriptions and custom questions
- **Question Types**: Support for multiple question types including short text, long text, numbers, multiple choice, and single choice
- **Input Validation**: Custom regex validation with error messages for enhanced data quality
- **Timer System**: Automatic campaign expiration with configurable durations
- **Interactive UI**: Discord Views with buttons and modals for seamless user interaction
- **Distribution Options**: Send campaigns to channels or DM users directly
- **Data Export**: Export responses to CSV format for external analysis
- **Statistics**: View campaign statistics and response rates

### Question Types Supported
1. **Short Text**: Brief text responses (up to 100 characters)
2. **Long Text**: Detailed text responses (up to 1000 characters)
3. **Number**: Numeric input with validation
4. **Multiple Choice**: Users can select multiple options from a list
5. **Single Choice**: Users select one option from a list

### Administrative Controls
- **Permission-based**: Requires `manage_guild` permission for campaign management
- **Campaign Lifecycle**: Create, start, stop, and delete campaigns
- **Response Management**: View individual responses and campaign statistics
- **Data Export**: Generate CSV files with all response data

## Installation

1. Place the `campaign_cog` folder in your Red-Discordbot's cogs directory
2. Use the command `[p]load campaign_cog` to load the cog
3. The cog will automatically register its configuration and be ready for use

## Command Reference

### Campaign Management

#### `[p]campaign create <name> <description>`
Creates a new feedback campaign.
- **name**: Campaign name (max 100 characters)
- **description**: Campaign description (max 500 characters)
- **Returns**: Campaign ID for future reference

#### `[p]campaign add_question <campaign_id> <type> <question_text>`
Adds a question to an existing campaign.
- **campaign_id**: The ID of the campaign
- **type**: Question type (short_text, long_text, number)
- **question_text**: The question to ask users

#### `[p]campaign add_choice_question <campaign_id> <type> <question_text> <options...>`
Adds a multiple choice or single choice question.
- **type**: multiple_choice or single_choice
- **options**: List of choices (2-10 options)

#### `[p]campaign add_validation <campaign_id> <question_id> <regex_pattern> [error_message]`
Adds input validation to a question using regex patterns.

#### `[p]campaign remove_question <campaign_id> <question_id>`
Removes a question from a campaign.

#### `[p]campaign list`
Lists all campaigns in the current server.

#### `[p]campaign view <campaign_id>`
Displays detailed information about a specific campaign.

#### `[p]campaign start <campaign_id> [duration]`
Starts a campaign with optional duration (e.g., 1d2h30m).

#### `[p]campaign stop <campaign_id>`
Stops an active campaign.

#### `[p]campaign delete <campaign_id>`
Permanently deletes a campaign and all associated data.

### Campaign Distribution

#### `[p]campaign send <campaign_id> [#channel]`
Sends the campaign message to a specified channel (or current channel if not specified).

#### `[p]campaign dm_all <campaign_id>`
Sends the campaign via DM to all server members (requires confirmation).

#### `[p]campaign dm_role <campaign_id> @role`
Sends the campaign via DM to all users with a specific role.

### User Interaction

#### `[p]campaign submit <campaign_id>`
Allows users to submit responses to an active campaign.

### Data Management

#### `[p]campaign export <campaign_id>`
Exports all campaign responses to a CSV file.

#### `[p]campaign stats <campaign_id>`
Displays campaign statistics including response rates and completion data.

#### `[p]campaign responses <campaign_id> [@user]`
Views responses for a campaign or a specific user.

## Usage Examples

### Creating a Simple Feedback Campaign

```
[p]campaign create "Server Feedback" "Help us improve our Discord server!"
# Returns: Campaign created with ID: abc12345

[p]campaign add_question abc12345 short_text "What do you like most about our server?"
[p]campaign add_question abc12345 long_text "What improvements would you suggest?"
[p]campaign add_choice_question abc12345 single_choice "How often do you visit our server?" "Daily" "Weekly" "Monthly" "Rarely"

[p]campaign start abc12345 7d
# Starts campaign for 7 days

[p]campaign send abc12345 #general
# Sends campaign to #general channel
```

### Adding Input Validation

```
[p]campaign add_question abc12345 short_text "What's your Discord username?"
[p]campaign add_validation abc12345 def67890 "^[a-zA-Z0-9_]{2,32}$" "Please enter a valid Discord username (2-32 characters, letters, numbers, and underscores only)"
```

### Exporting Data

```
[p]campaign export abc12345
# Generates and uploads a CSV file with all responses
```

## Technical Implementation

### Data Storage
The cog uses Red-Discordbot's built-in `config` system for persistent data storage. Data is organized as follows:

- **Campaigns**: Stored per guild with campaign configurations
- **Responses**: User responses linked to campaigns and users
- **Timers**: Active campaign timers for automatic expiration

### Discord UI Components
The cog leverages Discord's UI framework for interactive elements:

- **Views**: Persistent views with question buttons and submit functionality
- **Modals**: Pop-up forms for question responses
- **Buttons**: Interactive elements for question selection and submission

### Rate Limiting
The cog implements rate limiting for DM distribution to comply with Discord's API limits:
- 1-second delay between DMs
- Progress tracking for large distributions
- Error handling for failed DM attempts

## Configuration

### Permissions Required
- **Bot Permissions**: Send Messages, Use Slash Commands, Embed Links, Attach Files
- **User Permissions**: Manage Guild (for campaign management commands)

### Limitations
- Maximum 20 questions per campaign
- Maximum 25 buttons per Discord View (automatically handled)
- Maximum 10 options per choice question
- Text input limits: 100 characters (short), 1000 characters (long)

## Error Handling

The cog includes comprehensive error handling for:
- Invalid campaign IDs
- Permission errors
- Discord API rate limits
- Invalid input validation
- Campaign state conflicts (e.g., modifying active campaigns)

## Data Privacy and Security

- All data is stored locally using Red-Discordbot's config system
- User responses are linked to Discord user IDs
- Campaign creators can export and delete data as needed
- No external data transmission or storage

## Troubleshooting

### Common Issues

1. **Campaign not found**: Ensure the campaign ID is correct and the campaign exists in the current server
2. **Permission denied**: Verify the user has `manage_guild` permission
3. **DM failures**: Users may have DMs disabled or have blocked the bot
4. **View timeouts**: Discord Views may timeout; users can use the submit command directly

### Support

For technical support or bug reports, refer to the Red-Discordbot community resources or the cog's source code for debugging information.

## Version Information

- **Minimum Red Version**: 3.5.0
- **Minimum Python Version**: 3.8.0
- **Dependencies**: None (uses only Red-Discordbot built-in modules)

This documentation provides a comprehensive guide to using the Campaign Cog effectively for collecting user feedback in Discord servers.

27 changes: 19 additions & 8 deletions campaigns/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import uuid
import csv
import io
from datetime import datetime, timedelta
from datetime import datetime
from typing import Dict, List, Literal, Optional, Any

from dateutil.tz import tzlocal
import discord
from redbot.core import commands, config, checks
from redbot.core.bot import Red
from redbot.core.utils.chat_formatting import box, pagify
from redbot.core.commands.converter import parse_timedelta

from .views import CampaignView
Expand All @@ -19,8 +18,6 @@
class Campaign:
"""Represents a feedback campaign."""

__version__ = "1.0.0"

def __init__(self, campaign_id: str, guild_id: int, name: str, description: str):
self.campaign_id = campaign_id
self.guild_id = guild_id
Expand Down Expand Up @@ -106,6 +103,8 @@ def from_dict(cls, data: Dict[str, Any]) -> "Campaign":
class CampaignCog(commands.Cog):
"""A cog for creating and managing user feedback campaigns."""

__version__ = "1.0.1"

def __init__(self, bot: Red):
self.bot = bot
self.config = config.Config.get_conf(self, identifier=1234567890, force_registration=True)
Expand Down Expand Up @@ -454,10 +453,18 @@ async def view_campaign(self, ctx, campaign_id_or_name: str):
embed.add_field(name="Questions", value=str(len(campaign.questions)), inline=True)

if campaign.start_time:
embed.add_field(name="Started", value=campaign.start_time.strftime("%Y-%m-%d %H:%M UTC"), inline=True)
embed.add_field(
name="Started",
value=f"<t:{int(campaign.start_time.astimezone(tzlocal()).timestamp())}>",
inline=True,
)

if campaign.end_time:
embed.add_field(name="Expires", value=campaign.end_time.strftime("%Y-%m-%d %H:%M UTC"), inline=True)
embed.add_field(
name="Expires",
value=f"<t:{int(campaign.end_time.astimezone(tzlocal()).timestamp())}>",
inline=True,
)

# Show questions
if campaign.questions:
Expand Down Expand Up @@ -703,7 +710,9 @@ async def submit_campaign(self, ctx, campaign_id_or_name: str):
embed.add_field(name="Questions", value=f"{len(campaign.questions)} questions available", inline=True)

if campaign.end_time:
embed.add_field(name="Expires", value=campaign.end_time.strftime("%Y-%m-%d %H:%M UTC"), inline=True)
embed.add_field(
name="Expires", value=f"<t:{int(campaign.end_time.astimezone(tzlocal()).timestamp())}>", inline=True
)

if user_response:
answered_count = len(user_response.get("answers", {}))
Expand Down Expand Up @@ -756,7 +765,9 @@ async def send_campaign(self, ctx, campaign_id_or_name: str, channel: Optional[d
embed.add_field(name="Questions", value=f"{len(campaign.questions)} questions", inline=True)

if campaign.end_time:
embed.add_field(name="Expires", value=campaign.end_time.strftime("%Y-%m-%d %H:%M UTC"), inline=True)
embed.add_field(
name="Expires", value=f"<t:{int(campaign.end_time.astimezone(tzlocal()).timestamp())}>", inline=True
)

embed.add_field(
name="How to Participate",
Expand Down
4 changes: 2 additions & 2 deletions personalroles/personalroles.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ async def icon_emoji(self, ctx, *, emoji: Optional[Union[discord.Emoji, discord.
if isinstance(emoji, discord.Emoji) or isinstance(emoji, discord.PartialEmoji):
await emoji.save(emoji_bytes)
emoji_bytes.seek(0)
await role.edit(display_icon=emoji_bytes)
await role.edit(display_icon=emoji_bytes.getvalue())
elif emoji is not None:
emoji = emoji.split()[0]
if demojize(emoji) == emoji:
Expand All @@ -413,7 +413,7 @@ async def icon_emoji(self, ctx, *, emoji: Optional[Union[discord.Emoji, discord.
except discord.Forbidden:
ctx.command.reset_cooldown(ctx)
await ctx.send(chat.error(_("Unable to edit role.\nRole must be lower than my top role")))
except discord.InvalidArgument:
except TypeError:
await ctx.send(chat.error(_("This image type is unsupported, or link is incorrect")))
except discord.HTTPException as e:
ctx.command.reset_cooldown(ctx)
Expand Down