Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2850c7e630 | |||
| c252f4cc67 | |||
| 96a335bea6 | |||
| fb48a256ce | |||
| 0ceffca196 | |||
| 51911604a9 | |||
| 660341127d | |||
| d419a7f2d2 | |||
| b4b818a5c3 | |||
| b39bc5c16b | |||
| 130cc5370c |
@@ -0,0 +1,4 @@
|
||||
DISCORD_TOKEN=
|
||||
PYTHONPATH=./src
|
||||
CRYPT_KEY=
|
||||
LOG_DIR=logs
|
||||
@@ -0,0 +1,46 @@
|
||||
# About Analyst-bot's data usage
|
||||
|
||||
## TL;DR
|
||||
|
||||
Analyst-bot collects text message information. It does not share collected data with any third-party and data is retained 18 months or until the bot is leaving the guild/server.
|
||||
|
||||
## Data collection
|
||||
|
||||
Analyst-bot collects a Discord guild/server's history when asked to.
|
||||
|
||||
This includes:
|
||||
|
||||
- Visible text channel names
|
||||
- Visible text messages: date and time of creation and edition, author, content, reactions and other available metadata (pinned, tts, etc.)
|
||||
|
||||
This does __not__ includes:
|
||||
|
||||
- Voice channels and not visible channels
|
||||
- Not visible text messages
|
||||
- Visible text messages' embedded content, images and other attachments
|
||||
|
||||
## Data processing
|
||||
|
||||
Any data collected is only processed in order to produce a one-time report sent to the user immediately. No temporary data are retained.
|
||||
|
||||
## Data storage and retain policy
|
||||
|
||||
Analyst-bot stores the collected data in files that are accessible by the software and its administrator only.
|
||||
|
||||
Any collected data are retained maximum 30 days until deletion or when the bot is leaving a guild/server.
|
||||
|
||||
## Data sharing
|
||||
|
||||
Analyst-bot does not share the data collected with any third-party.
|
||||
|
||||
## Right to retract
|
||||
|
||||
If you want to have your data removed, you can use the `%gdpr revoke` command or remove this bot from your guild/server.
|
||||
|
||||
## Terms agreement
|
||||
|
||||
By agreeing to these terms, you ensure having the legal age if you are in a country that does have one and you also ensure having the consent of every member involved.
|
||||
|
||||
*If you want more information, please contact the creator of this bot: <https://github.com/Klemek/discord-analyst>.*
|
||||
|
||||
Type `%gdpr agree` to agree to these terms, `%gdpr revoke` to remove this guild/server's collected data or `%gdpr` to see this message again.
|
||||
@@ -125,11 +125,14 @@ python3 src/main.py
|
||||
|
||||
## Changelog
|
||||
|
||||
* **v1.17**
|
||||
* compliency with 30 days data keeping policy and data encryption
|
||||
* improvements and bug fix
|
||||
* **v1.16**
|
||||
* `%freq graph` graph hours frequency along the week
|
||||
* uses discord new time format
|
||||
* `%freq` now shows quietest day of week and hour of day
|
||||
* improvments and bug fix
|
||||
* improvements and bug fix
|
||||
* **v1.15**
|
||||
* `nsfw:allow/only` filter nsfw channels
|
||||
* `%find` can use regexes
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
discord.py==1.7.0
|
||||
python-dotenv==0.15.0
|
||||
python-dateutil==2.8.1
|
||||
git+git://github.com/Klemek/miniscord.git
|
||||
numpy
|
||||
matplotlib
|
||||
discord.py>=1.7.0
|
||||
python-dotenv>=0.15.0
|
||||
python-dateutil>=2.8.1
|
||||
matplotlib>=3.4.2
|
||||
cryptography>=2.8
|
||||
git+git://github.com/Klemek/miniscord.git
|
||||
@@ -2,7 +2,6 @@ from typing import List
|
||||
from datetime import timedelta
|
||||
import calendar
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from io import BytesIO
|
||||
import discord
|
||||
import time
|
||||
@@ -16,18 +15,6 @@ from utils import (
|
||||
mention,
|
||||
)
|
||||
|
||||
CB_color_cycle = [
|
||||
"#e41a1c",
|
||||
"#984ea3",
|
||||
"#377eb8",
|
||||
"#4daf4a",
|
||||
"#dede00",
|
||||
"#ff7f00",
|
||||
"#a65628",
|
||||
"#f781bf",
|
||||
"#999999",
|
||||
]
|
||||
|
||||
|
||||
class Frequency:
|
||||
def __init__(self):
|
||||
@@ -71,12 +58,7 @@ class Frequency:
|
||||
self.hours[i][0] * 7 / n_hours
|
||||
]
|
||||
ax.plot(
|
||||
times,
|
||||
hours,
|
||||
label=calendar.day_name[i],
|
||||
linestyle="--",
|
||||
linewidth=0.8,
|
||||
c=CB_color_cycle[i],
|
||||
times, hours, label=calendar.day_name[i], linestyle="--", linewidth=0.8
|
||||
)
|
||||
|
||||
hours = [day[hour] / n_hours for hour in range(24)] + [day[0] / n_hours]
|
||||
|
||||
+50
-20
@@ -8,28 +8,30 @@ import time
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from . import ChannelLogs
|
||||
from utils import code_message, delta, deltas
|
||||
|
||||
|
||||
LOG_DIR = "logs"
|
||||
LOG_EXT = ".logz"
|
||||
|
||||
current_analysis = []
|
||||
current_analysis_lock = threading.Lock()
|
||||
|
||||
|
||||
ALREADY_RUNNING = -100
|
||||
CANCELLED = -200
|
||||
NO_FILE = -300
|
||||
|
||||
# 5 minutes, assume 'fast' arg
|
||||
MIN_MODIFICATION_TIME = 5 * 60
|
||||
# ~1 year, remove log file
|
||||
MAX_MODIFICATION_TIME = 365 * 24 * 60 * 60
|
||||
load_dotenv()
|
||||
|
||||
LOG_DIR = os.getenv("LOG_DIR", "logs")
|
||||
LOG_EXT = os.getenv("LOG_EXT", ".logz")
|
||||
CRYPT_KEY = os.getenv("CRYPT_KEY", "")
|
||||
|
||||
# 5 minutes, assume 'fast' arg
|
||||
MIN_MODIFICATION_TIME = int(os.getenv("MAX_MODIFICATION_TIME", 5 * 60))
|
||||
|
||||
# 30 days, remove log file
|
||||
MAX_MODIFICATION_TIME = int(os.getenv("MAX_MODIFICATION_TIME", 30 * 24 * 60 * 60))
|
||||
|
||||
class Worker:
|
||||
def __init__(
|
||||
@@ -129,29 +131,41 @@ class GuildLogs:
|
||||
channels = {}
|
||||
try:
|
||||
last_time = os.path.getmtime(self.log_file)
|
||||
gziped_data = None
|
||||
await code_message(progress, "Reading saved history (1/4)...")
|
||||
encrypted_data = None
|
||||
await code_message(progress, "Reading saved history (1/5)...")
|
||||
t0 = datetime.now()
|
||||
with open(self.log_file, mode="rb") as f:
|
||||
gziped_data = f.read()
|
||||
encrypted_data = f.read()
|
||||
logging.info(f"log {self.guild.id} > read in {delta(t0):,}ms")
|
||||
if self.check_cancelled():
|
||||
return CANCELLED, 0
|
||||
await code_message(progress, "Reading saved history (2/4)...")
|
||||
await code_message(progress, "Reading saved history (2/5)...")
|
||||
if CRYPT_KEY == "" or CRYPT_KEY is None:
|
||||
gziped_data = encrypted_data
|
||||
try:
|
||||
t0 = datetime.now()
|
||||
fernet = Fernet(CRYPT_KEY)
|
||||
gziped_data = fernet.decrypt(encrypted_data)
|
||||
logging.info(f"log {self.guild.id} > decrypted in {delta(t0):,}ms")
|
||||
except:
|
||||
gziped_data = encrypted_data
|
||||
if self.check_cancelled():
|
||||
return CANCELLED, 0
|
||||
await code_message(progress, "Reading saved history (3/5)...")
|
||||
t0 = datetime.now()
|
||||
json_data = gzip.decompress(gziped_data)
|
||||
del gziped_data
|
||||
logging.info(f"log {self.guild.id} > gzip decompress in {delta(t0):,}ms")
|
||||
if self.check_cancelled():
|
||||
return CANCELLED, 0
|
||||
await code_message(progress, "Reading saved history (3/4)...")
|
||||
await code_message(progress, "Reading saved history (4/5)...")
|
||||
t0 = datetime.now()
|
||||
channels = json.loads(json_data)
|
||||
del json_data
|
||||
logging.info(f"log {self.guild.id} > json parse in {delta(t0):,}ms")
|
||||
if self.check_cancelled():
|
||||
return CANCELLED, 0
|
||||
await code_message(progress, "Reading saved history (4/4)...")
|
||||
await code_message(progress, "Reading saved history (5/5)...")
|
||||
t0 = datetime.now()
|
||||
self.channels = {
|
||||
int(id): ChannelLogs(channels[id], self) for id in channels
|
||||
@@ -288,7 +302,7 @@ class GuildLogs:
|
||||
return CANCELLED, 0
|
||||
await code_message(
|
||||
progress,
|
||||
f"Saving history (1/3)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
f"Saving history (1/4)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
)
|
||||
t0 = datetime.now()
|
||||
json_data = bytes(json.dumps(self.dict()), "utf-8")
|
||||
@@ -299,7 +313,7 @@ class GuildLogs:
|
||||
return CANCELLED, 0
|
||||
await code_message(
|
||||
progress,
|
||||
f"Saving history (2/3)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
f"Saving history (2/4)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
)
|
||||
t0 = datetime.now()
|
||||
gziped_data = gzip.compress(json_data)
|
||||
@@ -311,12 +325,28 @@ class GuildLogs:
|
||||
return CANCELLED, 0
|
||||
await code_message(
|
||||
progress,
|
||||
f"Saving history (3/3)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
f"Saving history (3/4)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
)
|
||||
if CRYPT_KEY == "" or CRYPT_KEY is None:
|
||||
encrypted_data = gziped_data
|
||||
try:
|
||||
t0 = datetime.now()
|
||||
fernet = Fernet(CRYPT_KEY)
|
||||
encrypted_data = fernet.encrypt(gziped_data)
|
||||
logging.info(f"log {self.guild.id} > encrypted in {delta(t0):,}ms -> {len(gziped_data) / deltas(t0):,.3f} b/s")
|
||||
except:
|
||||
encrypted_data = gziped_data
|
||||
if self.check_cancelled():
|
||||
return CANCELLED, 0
|
||||
await code_message(
|
||||
progress,
|
||||
f"Saving history (4/4)...\n{real_total_msg:,} messages in {real_total_chan:,} channels",
|
||||
)
|
||||
t0 = datetime.now()
|
||||
with open(self.log_file, mode="wb") as f:
|
||||
f.write(gziped_data)
|
||||
f.write(encrypted_data)
|
||||
del gziped_data
|
||||
del encrypted_data
|
||||
logging.info(
|
||||
f"log {self.guild.id} > saved in {delta(t0):,}ms -> {real_total_msg / deltas(t0):,.3f} m/s"
|
||||
)
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ emojis.load_emojis()
|
||||
|
||||
bot = Bot(
|
||||
"Discord Analyst",
|
||||
"1.16.1",
|
||||
"1.17.0",
|
||||
alias="%",
|
||||
)
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ class PresenceScanner(Scanner):
|
||||
for mention in message.mentions:
|
||||
pres.mention_others[mention] += 1
|
||||
pres.messages[message.author] += 1
|
||||
pres.channel_total[channel.id] += 1
|
||||
pres.mention_count[message.author] += len(message.mentions)
|
||||
pres.channel_total[channel.id] += 1
|
||||
pres.mention_count[message.author] += len(message.mentions)
|
||||
if len(raw_members) > 0:
|
||||
for mention in message.mentions:
|
||||
if mention in raw_members:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import os.path
|
||||
from dotenv import load_dotenv
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
load_dotenv()
|
||||
|
||||
LOG_DIR = os.getenv("LOG_DIR", "logs")
|
||||
LOG_EXT = os.getenv("LOG_DIR", ".logz")
|
||||
CRYPT_KEY = os.getenv("CRYPT_KEY", "")
|
||||
|
||||
fernet = Fernet(CRYPT_KEY)
|
||||
|
||||
for item in os.listdir(LOG_DIR):
|
||||
if item.endswith(LOG_EXT):
|
||||
path = os.path.join(LOG_DIR, item)
|
||||
data = None
|
||||
with open(path, mode="rb") as f:
|
||||
data = f.read()
|
||||
try:
|
||||
fernet.decrypt(data)
|
||||
print(f"{item} already encrypted")
|
||||
except:
|
||||
with open(path, mode="wb") as f:
|
||||
f.write(fernet.encrypt(data))
|
||||
print(f"{item} was encrypted")
|
||||
+1
-1
@@ -27,7 +27,7 @@ This does __not__ includes:
|
||||
Any data collected is only processed in order to produce a one-time report sent to the user immediately. No temporary data are retained.
|
||||
**Data storage and retain policy**
|
||||
Analyst-bot stores the collected data in files that are accessible by the software and its administrator only.
|
||||
Any collected data are retained maximum 18 months until deletion or when the bot is leaving a guild/server.
|
||||
Any collected data are retained maximum 30 days until deletion or when the bot is leaving a guild/server.
|
||||
**Data sharing**
|
||||
Analyst-bot does not share the data collected with any third-party.
|
||||
**Right to retract**
|
||||
|
||||
Reference in New Issue
Block a user