commit c48bf54dc138c7fe3b051020ed3619a83d607de2 Author: retoor Date: Wed Nov 27 02:59:42 2024 +0100 Initial commit diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..37efb31 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,22 @@ +name: Build Ragnar anti spam bot +run-name: Build Ragnar anti spam bot +on: [push] + +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: List files in the repository + run: | + ls ${{ gitea.workspace }} + - run: echo "Install dependencies." + - run: apt update + - run: apt install python3 python3-pip python3-venv make -y + - run: make + - run: git add . + - run: git config --global user.email "bot@molodetz.com" + - run: git config --global user.name "bot" + - run: git commit -a -m "Update export statistics" + - run: git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb26022 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +config.py +.venv +.history +src/ragnar/__pycache__ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..96ceb6c --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +all: ensure_env format build install + +format: + ./.venv/bin/python -m pip install black + ./.venv/bin/python -m black . + +ensure_env: + -@python3 -m venv .venv + +build: + ./.venv/bin/python -m pip install build + ./.venv/bin/python -m build . + +install: + ./.venv/bin/python -m pip install -e . + +run: + python -m ragnar.run \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aecbe8a --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Ragnar + +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. + +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. + +I expect this project tomorrow to have deployed fully functional on a server. + +## In progress + +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. + +## 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. + diff --git a/dist/Ragnar-1.3.37-py3-none-any.whl b/dist/Ragnar-1.3.37-py3-none-any.whl new file mode 100644 index 0000000..4c7a495 Binary files /dev/null and b/dist/Ragnar-1.3.37-py3-none-any.whl differ diff --git a/dist/ragnar-1.3.37.tar.gz b/dist/ragnar-1.3.37.tar.gz new file mode 100644 index 0000000..1824b12 Binary files /dev/null and b/dist/ragnar-1.3.37.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07de284 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..9cf2428 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = Ragnar +version = 1.3.37 +description = Anti spam bot for dR +author = Retoor +author_email = retoor@molodetz.nl +license = MIT +long_description = file: README.md +long_description_content_type = text/markdown + +[options] +packages = find: +package_dir = + = src +python_requires = >=3.7 +install_requires = + requests==2.32.3 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + ragnar.run = ragnar.cli:run \ No newline at end of file diff --git a/src/Ragnar.egg-info/PKG-INFO b/src/Ragnar.egg-info/PKG-INFO new file mode 100644 index 0000000..1667748 --- /dev/null +++ b/src/Ragnar.egg-info/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 2.1 +Name: Ragnar +Version: 1.3.37 +Summary: Anti spam bot for dR +Author: Retoor +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 diff --git a/src/Ragnar.egg-info/SOURCES.txt b/src/Ragnar.egg-info/SOURCES.txt new file mode 100644 index 0000000..16385dd --- /dev/null +++ b/src/Ragnar.egg-info/SOURCES.txt @@ -0,0 +1,14 @@ +pyproject.toml +setup.cfg +src/Ragnar.egg-info/PKG-INFO +src/Ragnar.egg-info/SOURCES.txt +src/Ragnar.egg-info/dependency_links.txt +src/Ragnar.egg-info/entry_points.txt +src/Ragnar.egg-info/requires.txt +src/Ragnar.egg-info/top_level.txt +src/ragnar/__init__.py +src/ragnar/__main__.py +src/ragnar/api.py +src/ragnar/bot.py +src/ragnar/cache.py +src/ragnar/cli.py \ No newline at end of file diff --git a/src/Ragnar.egg-info/dependency_links.txt b/src/Ragnar.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Ragnar.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/Ragnar.egg-info/entry_points.txt b/src/Ragnar.egg-info/entry_points.txt new file mode 100644 index 0000000..3a5bae1 --- /dev/null +++ b/src/Ragnar.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +ragnar.run = ragnar.cli:run diff --git a/src/Ragnar.egg-info/requires.txt b/src/Ragnar.egg-info/requires.txt new file mode 100644 index 0000000..8cf6b22 --- /dev/null +++ b/src/Ragnar.egg-info/requires.txt @@ -0,0 +1,3 @@ +aiohttp==3.10.10 +dataset==1.6.2 +requests==2.32.3 diff --git a/src/Ragnar.egg-info/top_level.txt b/src/Ragnar.egg-info/top_level.txt new file mode 100644 index 0000000..aed9676 --- /dev/null +++ b/src/Ragnar.egg-info/top_level.txt @@ -0,0 +1 @@ +ragnar diff --git a/src/ragnar/__init__.py b/src/ragnar/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ragnar/__main__.py b/src/ragnar/__main__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ragnar/api.py b/src/ragnar/api.py new file mode 100644 index 0000000..c16a5ec --- /dev/null +++ b/src/ragnar/api.py @@ -0,0 +1,96 @@ +import requests, json + +from ragnar.cache import method_cache + + +class Api: + + base_url = "https://www.devrant.io/api/" + + def __init__(self, username, password): + self.username = username + self.password = password + self.auth = None + + def post_comment(self, rant_id, text): + response = requests.post( + self.base_url + "devrant/rants/" + str(rant_id) + "/comments", + data={ + "app": 3, + "user_id": self.auth["user_id"], + "token_id": self.auth["token_id"], + "token_key": self.auth["token_key"], + "comment": text, + }, + ) + return response.json() + + @method_cache + def login(self): + print("New login, cache miss?") + rawdata = requests.post( + self.base_url + "users/auth-token", + data={"username": self.username, "password": self.password, "app": 3}, + ) + rawdata = rawdata.json() + if not rawdata["success"]: + self.auth = None + else: + self.auth = { + "token_id": rawdata["auth_token"]["id"], + "token_key": rawdata["auth_token"]["key"], + "user_id": rawdata["auth_token"]["user_id"], + } + return self.auth + + @method_cache + def get_profile(self, id_): + url = self.base_url + "users/" + str(id_) + params = { + "app": 3, + } + response = requests.get(url, params) + return json.loads(response.text)["profile"] + + def get_search(self, term): + url = self.base_url + "devrant/search" + params = {"app": 3, "term": term} + response = requests.get(url, params, timeout=5) + obj = json.loads(response.text) + return obj + + def post_rant_vote(self, id, vote): + response = requests.post( + 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, + }, + ) + return response.json() + + def get_rant(self, rant_id): + url = self.base_url + "devrant/rants/" + str(rant_id) + params = { + "app": 3, + } + response = requests.get(url, params, timeout=5) + return json.loads(response.text) + + def get_rants(self, sort, limit, skip): + url = self.base_url + "devrant/rants" + params = {"app": 3, "sort": sort, "limit": limit, "skip": skip} + + response = requests.get(url, params, timeout=5) + return json.loads(response.text)["rants"] + + @method_cache + def get_user_id(self, name): + url = self.base_url + "get-user-id" + params = {"app": 3, "username": name} + response = requests.get(url, params) + return json.loads(response.text).get("user_id", None) diff --git a/src/ragnar/bot.py b/src/ragnar/bot.py new file mode 100644 index 0000000..32e7bed --- /dev/null +++ b/src/ragnar/bot.py @@ -0,0 +1,93 @@ +from ragnar.api import Api +import time +import random +from ragnar.cache import method_cache + + +class Bot: + + def __init__(self, username, password): + self.username = username + self.password = password + self.name = self.username.split("@")[0] + + names = { + "no-spam": "anna", + "no-spam1": "ira", + "no-spam2": "katya", + "no-spam3": "nastya", + "no-spam4": "vira", + } + 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 = ["$", "crypto", "hacker", "recovery"] + + self.api = Api(username=self.username, password=self.password) + + def rsleepii(self): + time.sleep(random.randint(1, 3)) + + @method_cache + def login(self): + self.rsleepii() + self.auth = self.api.login() + if not self.auth: + print("Authentication for {} failed.".format(self.username)) + raise Exception("Login error") + print("Authentication succesful for {}.".format(self.username)) + + @method_cache + def is_sus_rant(self, rant_id, rant_text): + clean_text = rant_text.replace(" ", "").lower() + for trigger in self.triggers: + if trigger in clean_text: + return True + + 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.mark_text in comment.get("body", ""): + return True + return False + + @method_cache + def is_user_sus(self, username): + user_id = self.api.get_user_id(username) + profile = self.api.get_profile(user_id) + score = profile["score"] + if score < 5: + print("User {} is sus with his score of only {}.".format(username, score)) + return True + else: + return False + + def mark_as_sus(self, rant): + self.rsleepii() + self.api.post_comment(rant["id"], self.mark_text) + + def fight(self): + self.rsleepii() + rants = self.api.get_rants("recent", 5, 0) + for rant in rants: + if not self.is_user_sus(rant["user_username"]): + print("User {} is trusted.".format(rant["user_username"])) + continue + if not self.is_sus_rant(rant["id"], rant["text"]): + print("Rant by {} is not sus.".format(rant["user_username"])) + continue + if self.is_flagged_as_sus(rant["id"], rant.get("num_comments")): + continue + print("Rant is not {} flagged as sus yet.".format(rant["user_username"])) + print("Flagging rant by {} as sus.".format(rant["user_username"])) + self.mark_as_sus(rant) + self.down_vote_rant(rant) + + def down_vote_rant(self, rant): + print("Downvoting rant by {}.".format(rant["user_username"])) + print(self.api.post_rant_vote(rant["id"], 4)) diff --git a/src/ragnar/cache.py b/src/ragnar/cache.py new file mode 100644 index 0000000..a6f2e66 --- /dev/null +++ b/src/ragnar/cache.py @@ -0,0 +1,14 @@ +# The functools lru_cache didn't work well on a class so +# I had to create a custom cashing method. Which is fine. + + +def method_cache(func): + cache = {} + + def wrapper(*args, **kwargs): + key = (args, tuple(sorted(kwargs.items()))) + if key not in cache: + cache[key] = func(*args, **kwargs) + return cache[key] + + return wrapper diff --git a/src/ragnar/cli.py b/src/ragnar/cli.py new file mode 100644 index 0000000..2430706 --- /dev/null +++ b/src/ragnar/cli.py @@ -0,0 +1,42 @@ +import argparse +from ragnar.bot import Bot +import random +import time +from concurrent.futures import ThreadPoolExecutor as Executor + + +def parse_args(): + parser = argparse.ArgumentParser(description="Process username and password.") + + parser.add_argument("-u", "--username", required=True, help="Your username") + parser.add_argument("-p", "--password", required=True, help="Your password") + + return parser.parse_args() + + +def bot_task(username, password): + 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() + with Executor(4) as executor: + for x in range(1, 5): + username = "no-spam{}@molodetz.nl".format(str(x)) + password = args.password + time.sleep(1) + print("Starting bot {}.".format(username)) + executor.submit(bot_task, username, password) + executor.shutdown(wait=True) + + +def run(): + main()