Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/220 overwatch functions #234

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
44e8340
Add Overwatch profile rules cog.
portalBlock May 7, 2023
4678785
Add the log channel command to set the output channel (oops, missed t…
portalBlock May 7, 2023
a5ab5a5
Use the correct config access when loading rules.
portalBlock May 7, 2023
a6e0b00
Correct a breaking typo.
portalBlock May 7, 2023
0574223
Correct a missed config access error.
portalBlock May 7, 2023
7d3ec2a
Change matchers to rules for more clarity, remove some default config…
portalBlock May 7, 2023
586983c
Missed the adding logic while renaming before.
portalBlock May 7, 2023
5cce5b4
Add back the default config? I'm not even sure what's going on with t…
portalBlock May 7, 2023
1fe7a57
Fill out the default config more as a sample. Restore (and hopefully …
portalBlock May 15, 2023
29220d2
Add rule list command.
portalBlock May 15, 2023
f9ace2a
For a thing
portalBlock May 15, 2023
c76a81e
Fix a typo...
portalBlock May 15, 2023
44a8621
Rename ow_profile to owprofile and create owvoice cog base.
portalBlock May 15, 2023
66f5245
Implement Overwatch voice capabilities.
portalBlock May 16, 2023
b3e08e4
Attempt to fix a perfectly sensible error.
portalBlock May 16, 2023
1560083
IDE auto import at it again.
portalBlock May 16, 2023
5667859
Oh wait, I forgot to await.
portalBlock May 16, 2023
29b04e9
Fix owvoice cog issues.
portalBlock May 16, 2023
e38d167
Used a relative time not anchored time, oops.
portalBlock May 16, 2023
55c6fd4
Remove ow naming.
portalBlock May 20, 2023
5ae3dc5
Standardize the command to set log channels.
portalBlock May 20, 2023
6886791
Add the base for MessageWatch - config only, no logic.
portalBlock May 20, 2023
f3416a6
Complete most of the MessageWatch implementation. Entirely untested.
portalBlock May 20, 2023
1d2f20a
Make things async.
portalBlock May 20, 2023
ccaaa1a
Typo
portalBlock May 20, 2023
3c3bb23
Change context, update/standardize naming.
portalBlock May 21, 2023
d0385db
For to await
portalBlock May 21, 2023
7a33dd1
Typo
portalBlock May 21, 2023
fb2cb4e
Correct timezone for comparison
portalBlock May 21, 2023
eecbfe3
Correct timezone for comparison
portalBlock May 21, 2023
8790e4d
Update docs for PR.
portalBlock May 21, 2023
b06dd81
Meld all watcher cogs into one large watcher cog
portalBlock Oct 16, 2023
7541465
Fix imports
portalBlock Oct 16, 2023
41b4562
Implement PR feedback, reduce other duplicate code. Untested with sub…
portalBlock Oct 26, 2023
7d7f007
Fix arg parser for profile watcher.
portalBlock Oct 26, 2023
0299665
Correct format for profile rule to string conversion.
portalBlock Feb 24, 2024
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ Cogs for the [RED](https://github.com/Cog-Creators/Red-DiscordBot/)-based [Homel
- [Google](#google)
- [Latex](#latex)
- [Letters](#letters)
- [MessageWatch](#messagewatch)
- [Notes](#notes)
- [Penis](#penis)
- [Phishingdetection](#phishingdetection)
- [ProfileWatch](#profilewatch)
- [Purge](#purge)
- [Quotes](#quotes)
- [Reactrole](#reactrole)
Expand All @@ -39,6 +41,7 @@ Cogs for the [RED](https://github.com/Cog-Creators/Red-DiscordBot/)-based [Homel
- [Sentry](#sentry)
- [Timeout](#timeout)
- [Verify](#verify)
- [VoiceWatch](#voicewatch)
- [xkcd](#xkcd)
- [License](#license)
- [Contributing](#contributing)
Expand Down Expand Up @@ -79,8 +82,10 @@ A massive thank you to all who've helped out with this project ❤️
- **[Google](#google):** Send a google link to someone.
- **[LaTeX](#latex):** Render a LaTeX statement.
- **[Letters](#letters):** Outputs large emote letters/numbers from input text.
- **[MessageWatch](#messagewatch):** Analyzes message components to detect automated activity.
- **[Notes](#notes):** Manage notes and warnings against users.
- **[Penis](#penis):** Allows users to check the size of their penis.
- **[ProfileWatch](#profilewatch):** Analyzes profile data and alerts upon recognized patterns.
- **[Purge](#purge):** This will purge users based on criteria.
- **[Quotes](#quotes):** Allows users to quote other users' messages in a quotes channel.
- **[Reactrole](#reactrole):** Allows roles to be applied and removed using reactions.
Expand All @@ -89,6 +94,7 @@ A massive thank you to all who've helped out with this project ❤️
- **[Sentry](#sentry):** Send unhandled errors to sentry.
- **[Timeout](#timeout):** Manage users' timeout status.
- **[Verify](#verify):** Allows users to verify themselves.
- **[VoiceWatch](#voicewatch):** Analyzes voice channel activity and alerts upon suspicious patterns.
- **[xkcd](#xkcd):** Allows users to look at xkcd comics.

## Cog Documentation
Expand Down Expand Up @@ -187,6 +193,16 @@ This cog converts a string of letters/numbers into large emote letters ("regiona
`[p]letters I would like this text as emotes 123`
`[p]letters -raw I would like this text as raw emote code 123`

### MessageWatch

Analyzes message components to detect automated activity.

- `[p]messagewatch logchannel [channel]` - Set the log output channel to the current (or specified) channel.
- `[p]messagewatch fetchtime <float: time (milliseconds)>` - Set the recent message timeframe used in calculations.
- `[p]messagewatch frequencies embed <float: frequency>` - Set the maximum allowable frequency of embeds/attachments.
- `[p]messagewatch exemptions memberduration <int: time (hours)>` - Set the minimum membership duration to qualify for exemptions.
- `[p]messagewatch exemptions textmessages <float: frequency>` - Set the minimum text-only frequency for participation exemptions.

### Notes

Manage notes and warnings against users.
Expand All @@ -213,6 +229,15 @@ This cog allows users to check the size of their penis.

This cog automatically deletes any messages containing suspected phishing/scam links. This information is sourced from [phish.sinking.yachts](https://phish.sinking.yachts/)

### ProfileWatch

Analyzes profile data and alerts upon recognized name/nickname patterns.

- `[p]profilewatch logchannel [channel]` - Set the log output channel to the current (or specified) channel.
- `[p]profilewatch add <name> <pattern> <level: HIGH or LOW> <check nickname: YES or NO> <reason>` - Add/edit a rule with the specified name. Pattern is a python-style regex.
- `[p]profilewatch list` - List the current rules.
- `[p]profilewatch delete <name>` - Delete the specified rule.

### Purge

This cog will purge users that hold no roles as a way to combat accounts being created and left in an un-verified state.
Expand Down Expand Up @@ -297,6 +322,13 @@ This cog will allow users to prove they're not a bot by having to read rules and

Further configuration options can be seen with `[p]verify help`

### VoiceWatch

Analyzes voice channel activity and alerts upon suspicious patterns.

- `[p]voicewatch logchannel [channel]` - Set the log output channel to the current (or specified) channel.
- `[p]voicewatch time <int: time (hours)>` - Set/update the minimum hours users must be in the server before without triggering an alert.

### xkcd

This cog allows users to look at xkcd comics
Expand Down
7 changes: 7 additions & 0 deletions messagewatch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from redbot.core.bot import Red

from .messagewatch import MessageWatchCog


async def setup(bot: Red):
await bot.add_cog(MessageWatchCog(bot))
15 changes: 15 additions & 0 deletions messagewatch/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"author": [
"portalBlock"
],
"short": "Monitor the frequency of messages and embeds.",
"description": "Analyzes message and embed activity in channels to detect suspicious actions.",
"disabled": false,
"name": "messagewatch",
"tags": [
"utility",
"mod"
],
"install_msg": "Usage: `[p]messagewatch add`",
"min_bot_version": "3.5.1"
}
196 changes: 196 additions & 0 deletions messagewatch/messagewatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
from datetime import datetime, timedelta, timezone
from typing import Optional, List

import discord
from redbot.core import Config, checks
from redbot.core.bot import Red
from redbot.core import commands
from redbot.core.utils.mod import is_mod_or_superior


class MessageWatchCog(commands.Cog):
"""MessageWatch Cog"""

def __init__(self, bot: Red):
self.bot = bot
self.config = Config.get_conf(self, identifier=128986274420752384004)
self.embed_speeds = {}
default_guild_config = {
"logchannel": "", # Channel to send alerts to
"recent_fetch_time": 15000, # Time in milliseconds to fetch recent prior embed times used for calculations.
"frequencies": { # Collection of allowable frequencies
"embed": 1 # Allowable frequency for embeds
},
"exemptions": {
"member_duration": 30, # Minimum member joined duration required to qualify for any exemptions
"text_messages": 1, # Minimum text-only message frequency required to exempt a user
}
}

self.config.register_guild(**default_guild_config)

@checks.admin()
@commands.group("messagewatch", aliases=["mw"], pass_context=True)
async def _messagewatch(self, ctx: commands.Context):
pass

@_messagewatch.command(name="logchannel")
async def _messagewatch_logchannel(self, ctx: commands.Context, channel: Optional[discord.TextChannel]):
"""Set/update the channel to send message activity alerts to."""

chanId = ctx.channel.id
if channel:
chanId = channel.id
await self.config.guild(ctx.guild).logchannel.set(chanId)
await ctx.send("✅ Alert channel successfully updated!")

@_messagewatch.command(name="fetchtime")
async def _messagewatch_fetchtime(self, ctx: commands.Context, time: str):
"""Set/update the recent message fetch time (in milliseconds)."""
try:
val = float(time)
await self.config.guild(ctx.guild).recent_fetch_time.set(val)
await ctx.send("Recent message fetch time successfully updated!")
except ValueError:
await ctx.send("Recent message fetch time FAILED to update. Please specify a `float` value only!")

@_messagewatch.group("frequencies", aliases=["freq", "freqs"], pass_context=True)
async def _messagewatch_frequencies(self, ctx: commands.Context):
pass

@_messagewatch_frequencies.command(name="embed")
async def _messagewatch_frequencies_embed(self, ctx: commands.Context, frequency: str):
"""Set/update the allowable embed frequency."""
try:
portalBlock marked this conversation as resolved.
Show resolved Hide resolved
val = float(frequency)
await self.config.guild(ctx.guild).frequencies.embed.set(val)
await ctx.send("Allowable embed frequency successfully updated!")
except ValueError:
await ctx.send("Allowable embed frequency FAILED to update. Please specify a `float` value only!")

@_messagewatch.group("exemptions", aliases=["exempt", "exempts"], pass_context=True)
async def _messagewatch_exemptions(self, ctx: commands.Context):
pass

@_messagewatch_exemptions.command(name="memberduration", aliases=["md"])
async def _messagewatch_exemptions_memberduration(self, ctx: commands.Context, time: str):
"""Set/update the minimum member duration, in hours, to qualify for exemptions."""
try:
val = int(time)
await self.config.guild(ctx.guild).exemptions.member_duration.set(val)
await ctx.send("Minimum member duration successfully updated!")
except ValueError:
await ctx.send("Minimum member duration FAILED to update. Please specify a `integer` value only!")

@_messagewatch_exemptions.command(name="textmessages", aliases=["text"])
async def _messagewatch_expemptions_textmessages(self, ctx: commands.Context, frequency: str):
"""Set/update the minimum frequency of text-only messages to be exempt."""
try:
val = float(frequency)
await self.config.guild(ctx.guild).exemptions.text_messages.set(val)
await ctx.send("Text-only message frequency exemption successfully updated!")
except ValueError:
await ctx.send("Text-only message frequency exemption FAILED to update. Please specify a `float` value "
"only!")

@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if await is_mod_or_superior(self.bot, message): # Automatically exempt mods/admin
return
for i in range(len(message.attachments)):
await self.add_embed_time(message.guild, message.author, datetime.utcnow()) # TODO: Use message timestamp
for i in range(len(message.embeds)):
await self.add_embed_time(message.guild, message.author, datetime.utcnow()) # TODO: Use message timestamp
await self.analyze_speed(message.guild, message)

@commands.Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message):
if await is_mod_or_superior(self.bot, before): # Automatically exempt mods/admins
return
total_increase = len(after.attachments) - len(before.attachments)
total_increase += len(after.embeds) - len(before.attachments)
if total_increase > 0:
for i in range(total_increase):
await self.add_embed_time(after.guild, # Use the ctx guild because edits are inconsistent, TODO: Message time
after.author if after.author is not None else before.author, datetime.utcnow())
await self.analyze_speed(after.guild, after)

async def get_embed_times(self, guild: discord.Guild, user: discord.User) -> List[datetime]:
if guild.id not in self.embed_speeds:
self.embed_speeds[guild.id] = {}
if user.id not in self.embed_speeds[guild.id]:
self.embed_speeds[guild.id][user.id] = []
return self.embed_speeds[guild.id][user.id]

async def add_embed_time(self, guild: discord.Guild, user: discord.User, time: datetime):
await self.get_embed_times(guild, user) # Call to get the times to build the user's cache if not already exists
self.embed_speeds[guild.id][user.id].append(time)

async def get_recent_embed_times(self, guild: discord.Guild, user: discord.User) -> List[datetime]:
filter_time = datetime.utcnow() - timedelta(milliseconds=await self.config.guild(guild).recent_fetch_time())
return [time for time in await self.get_embed_times(guild, user) if time >= filter_time]

async def analyze_speed(self, guild: discord.Guild, trigger: discord.Message):
"""Analyzes the frequency of embeds & attachments by a user. Should only be called upon message create/edit."""

embed_times = await self.get_recent_embed_times(guild, trigger.author)

# Check if we have enough basic data to calculate the frequency (prevents some config fetches below)
if len(embed_times) < 2:
return

# This is a bit of a hack but check if the total embeds, regardless of times, could exceed the frequency limit
# This is needed because one message with N > 1 embeds and no prior embed times would always trigger.
allowable_embed_frequency = await self.config.guild(guild).frequencies.embed()
fetch_time = await self.config.guild(guild).recent_fetch_time()
if len(embed_times) < allowable_embed_frequency * fetch_time:
return

first_time = embed_times[0]
last_time = embed_times[len(embed_times) - 1]
embed_frequency = len(embed_times) / ((last_time - first_time).microseconds / 1000) # convert to milliseconds
if embed_frequency > allowable_embed_frequency:
# Alert triggered, send unless exempt

# Membership duration exemption
allowable = trigger.author.joined_at + timedelta(
hours=await self.config.guild(guild).exemptions.member_duration())
if datetime.now(timezone.utc) < allowable: # Todo: this isn't supposed to exempt them, just allow exempts
# Text-only message exemption (aka active participation exemption)
# TODO
return

# No exemptions at this point, alert!
# Credit: Taken from report Cog
log_id = await self.config.guild(guild).logchannel()
log = None
if log_id:
log = guild.get_channel(log_id)
if not log:
# Failed to get the channel
return

data = self.make_alert_embed(trigger.author, trigger)

mod_pings = " ".join(
[i.mention for i in log.members if not i.bot and str(i.status) in ["online", "idle"]])
if not mod_pings: # If no online/idle mods
mod_pings = " ".join([i.mention for i in log.members if not i.bot])

await log.send(content=mod_pings, embed=data)
# End credit
def make_alert_embed(self, member: discord.Member, message: discord.Message) -> discord.Embed:
"""Construct the alert embed to be sent"""
# Copied from the report Cog.
return (
discord.Embed(
colour=discord.Colour.orange(),
description="High frequency of embeds detected from a user."
)
.set_author(name="Suspicious User Activity", icon_url=member.avatar.url)
.add_field(name="Server", value=member.guild.name)
.add_field(name="User", value=member.mention)
.add_field(name="Message",
value=f"https://discord.com/channels/{message.guild.id}/{message.channel.id}/{message.id}")
.add_field(name="Timestamp", value=f"<t:{int(datetime.now().utcnow().timestamp())}:F>")
)
7 changes: 7 additions & 0 deletions profilewatch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from redbot.core.bot import Red

from .profilewatch import ProfileWatchCog


async def setup(bot: Red):
await bot.add_cog(ProfileWatchCog(bot))
15 changes: 15 additions & 0 deletions profilewatch/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"author": [
"portalBlock"
],
"short": "Monitor profile names for patterns.",
"description": "Utilize regex pattern matching to check new users' names and nicknames.",
"disabled": false,
"name": "profilewatch",
"tags": [
"utility",
"mod"
],
"install_msg": "Usage: `[p]profilewatch add`",
"min_bot_version": "3.5.1"
}
Loading