Compare commits

..
5 Commits
Author SHA1 Message Date
retoor ac851d76c3 Added logging and rant_history
Build Ragnar anti spam bot / Build (push) Successful in 1m2s
2024-11-27 11:46:02 +01:00
retoor dba78e616c Added decent logging 2024-11-27 11:28:05 +01:00
retoor 8b4d553772 Added tests
Build Ragnar anti spam bot / Build (push) Successful in 57s
2024-11-27 10:56:37 +01:00
retoor 42eca170a6 Added unit test and regex
Build Ragnar anti spam bot / Build (push) Failing after 1m1s
2024-11-27 10:54:27 +01:00
retoor c48bf54dc1 Initial commit
Build Ragnar anti spam bot / Build (push) Successful in 1m31s
2024-11-27 02:59:42 +01:00
24 changed files with 109 additions and 432 deletions
-1
View File
@@ -20,4 +20,3 @@ jobs:
- run: git config --global user.name "bot"
- run: git commit -a -m "Update export statistics"
- run: git push
- run: echo "This job's status is ${{ job.status }}."
+1 -4
View File
@@ -1,7 +1,4 @@
config.py
.venv
.history
__pycache__/
errors.log
ragnar.log
.build-trigger-2024-12-02 14:26
src/ragnar/__pycache__
+7 -10
View File
@@ -1,24 +1,21 @@
ENV=./.venv/bin/activate
PYTHON=./.venv/bin/python
BIN=./.venv/bin
all: ensure_env format build install test
format:
$(PYTHON) -m pip install shed
. $(ENV) && python -m shed
./.venv/bin/python -m pip install black
./.venv/bin/python -m black .
ensure_env:
-@python3 -m venv .venv
build:
$(PYTHON) -m pip install build
$(PYTHON) -m build .
./.venv/bin/python -m pip install build
./.venv/bin/python -m build .
install:
$(PYTHON) -m pip install -e .
./.venv/bin/python -m pip install -e .
run:
$(BIN)/ragnar.run
python -m ragnar.run
test:
$(PYTHON) -m unittest ragnar.tests
./.venv/bin/python -m unittest ragnar.tests
+19 -78
View File
@@ -1,87 +1,28 @@
# Ragnar Bot: Automated Moderation System for Devrant.io
# Ragnar
## Overview
Ragnar Bot is an automated system designed to interact with the **Devrant.io API** for community moderation. It detects spam, flags inappropriate content, and takes corrective actions such as downvoting and commenting. The system is scalable, customizable, and supports concurrent operations for multiple users.
This is an anti spam bot network. It is named after the viking for no obvious reason.
---
I'm not happy about the quality of the source and it is not a representation of my usual work. If I would've spend more efford there would be some types and I've would use aiohttp and would've used context managers for example. Despite the source lacking a certain quality, the bots work great and are made not to be annoying to the server by not connecting all at once and caching certain things like user profile / user id and if a reand already is flaged for example to not annoy the server.
## Features
The bots have user name no-spam[1-4] but flag under a Russian girl name, also for no obvious reason. I liked it more than some technical name. Will probably rename the bots later. Could be that devRants prevents me to do that within a half year. It doesn't matter much, if the bots do a good job, we will barely see them.
### API Integration
- **User Authentication**: Secure login with credentials.
- **Profile Retrieval**: Access user profile data.
- **Content Search**: Find posts matching specific keywords or patterns.
- **Comments Management**: Post and fetch comments on rants.
- **Voting System**: Upvote or downvote rants programmatically.
- **Fetch Rants**: Retrieve and sort recent rants for analysis.
I expect this project tomorrow to have deployed fully functional on a server.
### Spam Detection
- **Keyword Triggers**: Detect spam using predefined keywords like `crypto`, `bitcoin`, and more.
- **Regex Matching**: Identify suspicious patterns in text, such as URLs or phone numbers.
- **Heuristic Checks**: Evaluate user profiles and comments for unusual behavior.
## In progress
### Automation
- **Flagging Spam**: Automatically downvote and comment on posts flagged as spam.
- **Concurrent Execution**: Operates multiple bots simultaneously using multithreading.
- **Customizable Triggers**: Adjust detection patterns to fit evolving spam trends.
The bots work perfect in sense that they're doing what they're programmed to do.
But the programming is not finished yet:
- the criteria can be better, tips how to optimize are very welcome.
- at this moment, they can only flag, useless, but we will have indication of future content to be cancelled. Every spam message should have a flag. If not, contact @retoor.
- the downvote function doesn't work because I couldn't figure out what value I had to post. Who knows it? After this, it's kinda done.
- a decent deployment on my server. Now it runs on my laptop because it's not done yet and it got late.
### Logging & Caching
- **Logging**: Outputs activity logs to both the console and a file for debugging and monitoring.
- **Caching**: Uses a custom caching system to optimize performance and reduce redundant operations.
---
## How It Works
### Components
1. **API**:
Handles all interactions with the Devrant.io platform, including user authentication, content retrieval, and posting actions.
2. **Bot**:
Implements moderation logic:
- Identifies suspicious content.
- Posts comments warning users about flagged content.
- Maintains a history of checked rants to avoid redundant processing.
3. **CLI**:
Command-line interface to configure and execute the bot. Accepts user credentials and runs bots for multiple users simultaneously.
4. **Victoria Integration**:
Fetches user-related data from a remote service (`victoria.molodetz.nl`) for enhanced operations.
### Workflow
1. The bot authenticates using the provided username and password.
2. It retrieves and evaluates recent rants for spam indicators.
3. If spam is detected:
- Posts a predefined warning comment.
- Downvotes the rant.
4. Maintains logs and runs continuously to monitor new rants.
---
## Installation & Usage
1. **Setup**:
Clone the repository and ensure all dependencies are installed.
2. **Run the Bot**:
Execute the bot via the CLI:
```bash
python cli.py -u <username> -p <password>
---
## Fan art
This is some fan art made by Buffon, a contributor of this project:
![Image generated by Buffon](buffon.jpg)
Another one:
![Image generated by Buffon](buffon2.jpg)
Third one, the legendary bot war of 2024 ended in peace.
![Image generated by Buffon](buffon3.jpg)
## How they work
One process starts four bots named no-spam[1-4]. These bots look at new rants.
If there is a new rant:
1. check if user has more than five posts. If so, it will not be seen as spam.
2. it will check certain keywords like hacker / money crypto related if so continue to step 3.
3. user will be informed by the bots that his rant is flagged and what to do about it.
4. rant will be downvoted by the four bots making it disappear.
-5
View File
@@ -1,5 +0,0 @@
2024-12-17 13:06:42,285 - __main__ - DEBUG - This is a debug message
2024-12-17 13:06:42,286 - __main__ - INFO - This is an info message
2024-12-17 13:06:42,286 - __main__ - WARNING - This is a warning message
2024-12-17 13:06:42,286 - __main__ - ERROR - This is an error message
2024-12-17 13:06:42,286 - __main__ - CRITICAL - This is a critical message
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
-29
View File
@@ -1,29 +0,0 @@
import logging
# Create or get the root logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # Set the logging level
# Create a file handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG) # Set the level for this handler
# Create a console handler (optional, for logging to the console too)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Create a logging format
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Add the handlers to the logger
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# Log some messages
logger.debug("This is a debug message")
logger.info("This is an info message")
logger.warning("This is a warning message")
logger.error("This is an error message")
logger.critical("This is a critical message")
+3 -89
View File
@@ -1,4 +1,4 @@
Metadata-Version: 2.4
Metadata-Version: 2.1
Name: Ragnar
Version: 1.3.37
Summary: Anti spam bot for dR
@@ -7,92 +7,6 @@ Author-email: retoor@molodetz.nl
License: MIT
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: aiohttp==3.10.10
Requires-Dist: dataset==1.6.2
Requires-Dist: requests==2.32.3
# Ragnar Bot: Automated Moderation System for Devrant.io
## Overview
Ragnar Bot is an automated system designed to interact with the **Devrant.io API** for community moderation. It detects spam, flags inappropriate content, and takes corrective actions such as downvoting and commenting. The system is scalable, customizable, and supports concurrent operations for multiple users.
---
## Features
### API Integration
- **User Authentication**: Secure login with credentials.
- **Profile Retrieval**: Access user profile data.
- **Content Search**: Find posts matching specific keywords or patterns.
- **Comments Management**: Post and fetch comments on rants.
- **Voting System**: Upvote or downvote rants programmatically.
- **Fetch Rants**: Retrieve and sort recent rants for analysis.
### Spam Detection
- **Keyword Triggers**: Detect spam using predefined keywords like `crypto`, `bitcoin`, and more.
- **Regex Matching**: Identify suspicious patterns in text, such as URLs or phone numbers.
- **Heuristic Checks**: Evaluate user profiles and comments for unusual behavior.
### Automation
- **Flagging Spam**: Automatically downvote and comment on posts flagged as spam.
- **Concurrent Execution**: Operates multiple bots simultaneously using multithreading.
- **Customizable Triggers**: Adjust detection patterns to fit evolving spam trends.
### Logging & Caching
- **Logging**: Outputs activity logs to both the console and a file for debugging and monitoring.
- **Caching**: Uses a custom caching system to optimize performance and reduce redundant operations.
---
## How It Works
### Components
1. **API**:
Handles all interactions with the Devrant.io platform, including user authentication, content retrieval, and posting actions.
2. **Bot**:
Implements moderation logic:
- Identifies suspicious content.
- Posts comments warning users about flagged content.
- Maintains a history of checked rants to avoid redundant processing.
3. **CLI**:
Command-line interface to configure and execute the bot. Accepts user credentials and runs bots for multiple users simultaneously.
4. **Victoria Integration**:
Fetches user-related data from a remote service (`victoria.molodetz.nl`) for enhanced operations.
### Workflow
1. The bot authenticates using the provided username and password.
2. It retrieves and evaluates recent rants for spam indicators.
3. If spam is detected:
- Posts a predefined warning comment.
- Downvotes the rant.
4. Maintains logs and runs continuously to monitor new rants.
---
## Installation & Usage
1. **Setup**:
Clone the repository and ensure all dependencies are installed.
2. **Run the Bot**:
Execute the bot via the CLI:
```bash
python cli.py -u <username> -p <password>
---
## Fan art
This is some fan art made by Buffon, a contributor of this project:
![Image generated by Buffon](buffon.jpg)
Another one:
![Image generated by Buffon](buffon2.jpg)
Third one, the legendary bot war of 2024 ended in peace.
![Image generated by Buffon](buffon3.jpg)
-4
View File
@@ -1,4 +1,3 @@
README.md
pyproject.toml
setup.cfg
src/Ragnar.egg-info/PKG-INFO
@@ -13,6 +12,3 @@ src/ragnar/api.py
src/ragnar/bot.py
src/ragnar/cache.py
src/ragnar/cli.py
src/ragnar/victoria.py
src/ragnar/tests/__init__.py
src/ragnar/tests/bot.py
+2
View File
@@ -1 +1,3 @@
aiohttp==3.10.10
dataset==1.6.2
requests==2.32.3
+3 -4
View File
@@ -1,13 +1,12 @@
import logging
import sys
file_handler = logging.FileHandler("ragnar.log")
file_handler.setLevel(logging.DEBUG)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(sys.stdout), file_handler],
handlers=[
logging.StreamHandler(sys.stdout),
]
)
log = logging.getLogger(__name__)
+7 -11
View File
@@ -1,10 +1,7 @@
import json
import requests, json
import requests
from ragnar import log
from ragnar.cache import method_cache
from ragnar import log
class Api:
@@ -30,7 +27,7 @@ class Api:
@method_cache
def login(self):
log.info(f"Logged in as {self.username}")
log.info("Logged in as {}".format(self.username))
rawdata = requests.post(
self.base_url + "users/auth-token",
data={"username": self.username, "password": self.password, "app": 3},
@@ -62,20 +59,19 @@ class Api:
obj = json.loads(response.text)
return obj
def post_rant_vote(self, id_, vote):
def post_rant_vote(self, id, vote):
response = requests.post(
self.base_url + "devrant/rants/" + str(id_) + "/vote",
self.base_url + "devrant/rants/" + str(id) + "/vote",
data={
"app": 3,
"user_id": self.auth["user_id"],
"token_id": self.auth["token_id"],
"token_key": self.auth["token_key"],
"vote": vote,
"plat": 3,
"reason": 2,
# "plat": 3,
},
)
return json.loads(response.text)
return response.json()
def get_rant(self, rant_id):
url = self.base_url + "devrant/rants/" + str(rant_id)
+42 -159
View File
@@ -1,32 +1,28 @@
import re
import time
from ragnar import log
from ragnar.api import Api
import time
import random
from ragnar.cache import method_cache
from ragnar.victoria import vic
import re
from ragnar import log
class Bot:
rant_history = []
def __init__(self, username, password):
self.username = username
self.password = password
self.name = self.username.split("@")[0]
self.amount_of_rants_to_check = 30
self.sus_users = vic.get_spammers()
self.names = {
"no-spam": "Anna",
"no-spam1": "Ira",
"no-spam2": "Katya",
"no-spam3": "Nastya",
"no-spam4": "Vira",
self.rant_history = []
names = {
"no-spam": "anna",
"no-spam1": "ira",
"no-spam2": "katya",
"no-spam3": "nastya",
"no-spam4": "vira",
}
self.name = self.names.get(self.name, self.username.split("@")[0])
self.mark_text = f"I am {self.name} and downvoted this post because I consider it spam. Your message will be removed from this community site due too much downvotes. See my profile for more information. Read my source code mentioned on my profile to see what you did wrong. Should be no problem for a developer.\n\nHave a nice day!\n\n\nIf your post is not spam, please mention @retoor."
self.name = names.get(self.name, "everyone")
self.mark_text = "You rant is flagged as spam by {}. Read bot source code to find out how to prevent this. Have a nice day! (Bot does not downvote yet, couldn't figure out what the downvote value should be. Upvote these bots btw so they can post a link. They're quite effective, they'll end spam.)".format(
self.name
)
self.auth = None
self.triggers = [
"$",
@@ -34,200 +30,87 @@ class Bot:
"hacker",
"recovery",
{"regex": r"\([+,(,0-9,),-]{7,}"},
{"regex": r"([\w\d]\.[\w\d]{2,5}\/[\w\d]+|http|www)"},
"money",
"landscape",
"cyber",
"recover",
"trust",
"bitcoin",
"wizard",
"diamond",
"carat",
"carats",
"leading",
"cheating",
"spouse",
"spy",
# "helping",
"unqiue",
"@ragnar",
"brides",
"singles",
"course",
"dating",
]
self.api = Api(username=self.username, password=self.password)
def rsleepii(self):
time.sleep(1)
time.sleep(random.randint(1, 3))
@method_cache
def login(self):
self.rsleepii()
self.auth = self.api.login()
if not self.auth:
log.error(f"Authentication for {self.username} failed.")
log.error("Authentication for {} failed.".format(self.username))
raise Exception("Login error")
log.info(f"Authentication succesful for {self.username}.")
log.info("Authentication succesful for {}.".format(self.username))
def clean_rant_text(self, rant_text):
return rant_text.replace(" ", "").lower()
def get_mentions(self, rant_text):
return re.findall(r"@\w+", rant_text)
def is_comment_deletable(self, rant_id, username):
rant = self.api.get_rant(rant_id)
for comment in rant.get("comments", []):
print("Checking if sus comment: ", comment["body"])
if (
comment.get("user_username") == username
and comment.get("user_score") <= 10
):
vic.register_spammer(comment["user_username"])
return True
return False
@method_cache
def is_sus_content(self, content):
clean_text = self.clean_rant_text(content)
def is_sus_rant(self, rant_id, rant_text):
clean_text = self.clean_rant_text(rant_text)
for trigger in self.triggers:
if type(trigger) == dict:
if trigger.get("regex"):
regex = trigger["regex"]
if re.search(regex, clean_text):
log.info(f"Regex trigger {regex} matched!")
log.info("Regex trigger {} matched!".format(regex))
return True
elif trigger in clean_text:
if trigger == "ragnar":
mentions = self.get_mentions(content)
for mention in mentions:
if mention == "@ragnar":
continue
username = mention.strip("@")
if self.is_comment_deletable(username):
vic.register_spammer(mention.strip("@"))
self.api.post_comment(
rant_id,
f"User {username} is sucessfully registered as spammer.",
)
else:
self.api.post_comment(
rant_id,
f"Can't register user {username} as spammer. User is trusted.",
)
if len(mentions) > 1:
return False
log.info(f"Trigger {trigger} matched!")
log.info("Trigger {} matched!".format(trigger))
return True
return False
@method_cache
def is_sus_rant(self, rant_id, rant_text):
return self.is_sus_content(rant_text)
def is_flagged_as_sus(self, rant_id, num_comments):
if not num_comments:
return False
self.rsleepii()
rant = self.api.get_rant(rant_id)
for comment in rant.get("comments", []):
if self.names.get(self.username, "") in comment.get("body", ""):
if self.mark_text in comment.get("body", ""):
return True
return False
@method_cache
def is_user_sus(self, username):
if username in self.sus_users:
return True
user_id = self.api.get_user_id(username)
profile = self.api.get_profile(user_id)
score = profile["score"]
if score < 5:
log.warning(f"User {username} is sus with his score of only {score}.")
log.warning("User {} is sus with his score of only {}.".format(username, score))
return True
else:
return False
def is_comments_sus(self, rant_id):
log.info(f"Checking if comments are sus of rant {rant_id}.")
def mark_as_sus(self, rant):
self.rsleepii()
rant = self.api.get_rant(rant_id)
for comment in rant.get("comments", []):
for spammer in self.sus_users:
if comment["user_username"] == spammer:
vic.downvote_comment(comment["id"])
return False
print("Checking if sus comment: ", comment["body"])
if "@ragnar" in comment.get("body", "").lower():
print("Ragnar is mentioned, so flagging as sus comment.")
return True
if comment["user_score"] >= 5:
print("User has reputation >= 5 so not sus.")
continue
if self.is_sus_content(comment.get("body", "")):
return True
return False
def mark_as_sus(self, rant, is_flagged_by_ai):
mark_text = self.mark_text
if is_flagged_by_ai:
mark_text += "\n* Flagged by AI."
self.api.post_comment(rant["id"], mark_text)
self.api.post_comment(rant["id"], self.mark_text)
def fight(self):
self.rsleepii()
rants = self.api.get_rants("recent", self.amount_of_rants_to_check, 0)
rants = self.api.get_rants("recent", 5, 0)
for rant in rants:
if rant["id"] in self.rant_history:
log.debug("{}: Already checked rant {}.".format(self.name, rant["id"]))
if rant['id'] in self.rant_history:
log.debug("{}: Already checked rant {}.".format(self.name,rant['id']))
continue
flagged_by_id = False
else:
self.rant_history.append(rant['id'])
if not self.is_user_sus(rant["user_username"]):
log.info(
"{}: User {} is trusted.".format(self.name, rant["user_username"])
)
self.rant_history.append(rant["id"])
log.info("{}: User {} is trusted.".format(self.name, rant["user_username"]))
continue
elif self.is_comments_sus(rant["id"]):
log.info("Comments of rant are sus + user sus. Will flag as spam.")
elif rant["user_username"] in self.sus_users:
pass
elif not self.is_sus_rant(rant["id"], rant["text"]):
if not self.is_user_sus(rant["user_username"]):
continue
log.info(
"{}: Rant by {} is not sus by traditional method..".format(
self.name, rant["user_username"]
)
)
if vic.is_spam(rant["text"]) <= 5:
self.rant_history.append(rant["id"])
continue
else:
log.info(
"{}: Rant by {} is sus according to AI.".format(
self.name, rant["user_username"]
)
)
log.warning(
"{}: Rant by {} is not flagged as sus yet but should be.".format(
self.name, rant["user_username"]
)
)
log.warning(
"{}: Flagging rant by {} as sus.".format(
self.name, rant["user_username"]
)
)
self.mark_as_sus(rant, flagged_by_id)
if not self.is_sus_rant(rant["id"], rant["text"]):
log.info("{}: Rant by {} is not sus.".format(self.name, rant["user_username"]))
continue
if self.is_flagged_as_sus(rant["id"], rant.get("num_comments")):
continue
log.warning("{}: Rant is not {} flagged as sus yet.".format(self.name,rant["user_username"]))
log.warning("{}: Flagging rant by {} as sus.".format(self.name, rant["user_username"]))
self.mark_as_sus(rant)
self.down_vote_rant(rant)
self.rant_history.append(rant["id"])
def down_vote_rant(self, rant):
vic.dr_downvote_rant(rant["id"], 5)
log.warning("Downvoting rant by {} for 5 times.".format(rant["user_username"]))
log.debug("Vote result: ".format(self.api.post_rant_vote(rant["id"], -1)))
log.warning("Downvoting rant by {}.".format(rant["user_username"]))
log.debug(self.api.post_rant_vote(rant["id"], 4))
+23 -19
View File
@@ -1,9 +1,9 @@
import argparse
import time
from ragnar import log
from ragnar.bot import Bot
import random
import time
from concurrent.futures import ThreadPoolExecutor as Executor
from ragnar import log
def parse_args():
parser = argparse.ArgumentParser(description="Process username and password.")
@@ -14,23 +14,27 @@ def parse_args():
return parser.parse_args()
def bot_task(username, password):
log.info("Created new bot runniner. Username: {}".format(username))
time.sleep(random.randint(1, 20))
bot = Bot(username=username, password=password)
bot.login()
while True:
time.sleep(random.randint(1, 20))
try:
bot.fight()
except Exception as ex:
print(ex)
def main():
args = parse_args()
usernames = ["no-spam1", "no-spam2", "no-spam3", "no-spam4"]
while True:
for username in usernames:
time_start = time.time()
try:
log.info(f"Created new bot runner. Username: {username}")
bot = Bot(username=username, password=args.password)
bot.login()
bot.fight()
except Exception as ex:
log.critical(ex, exc_info=True)
time.sleep(1)
time_duration = time.time() - time_start
log.info(f"Bot {username} finished in {time_duration} seconds.")
with Executor(4) as executor:
for x in range(1, 5):
username = "no-spam{}@molodetz.nl".format(str(x))
password = args.password
executor.submit(bot_task, username, password)
executor.shutdown(wait=True)
def run():
Binary file not shown.
-13
View File
@@ -1,5 +1,4 @@
import unittest
from ragnar.bot import Bot
@@ -15,15 +14,3 @@ class BotTestCase(unittest.TestCase):
def test_is_sus_rant_regex_match(self):
rant_text = "To learn more about our services or to schedule a consultation, contact us at +1 (604) 200-0581 today."
self.assertTrue(self.bot.is_sus_rant(42, rant_text))
def test_is_sus_rant_regex_url_match_dot_slash(self):
rant_text = "Visit Now: thesleepcompany.in/pages/all-chairs"
self.assertTrue(self.bot.is_sus_rant(42, rant_text))
def test_is_sus_rant_regex_url_match_http_or_www(self):
rant_text = "http:// www . google . nl"
self.assertTrue(self.bot.is_sus_rant(42, rant_text))
def test_is_sus_content(self):
rant_text = "I completely understand the frustration—its such a hassle to manage all those details manually. I recently started using a platform called best ai trip planner https://easytrip.ai/ and its been a game-changer for my travel planning. This AI-powered service analyzes your trip requirements and gives you a comprehensive plan with the best transportation options. Whether youre looking for the fastest way to travel, the most scenic routes, or something within a specific budget, EasyTrip.ai does all the heavy lifting for you. Its also great for last-minute plans because it quickly pulls together all the available options!"
self.assertTrue(self.bot.is_sus_content(rant_text))
-3
View File
@@ -1,3 +0,0 @@
from xmlrpc.client import ServerProxy
vic = ServerProxy("https://victoria.molodetz.nl/rpc")
-1
View File
@@ -1 +0,0 @@
aaltink*132