Initial commit

This commit is contained in:
retoor 2024-11-27 02:59:42 +01:00
commit c48bf54dc1
20 changed files with 377 additions and 0 deletions

View File

@ -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

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
config.py
.venv
.history
src/ragnar/__pycache__

18
Makefile Normal file
View File

@ -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

28
README.md Normal file
View File

@ -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.

BIN
dist/Ragnar-1.3.37-py3-none-any.whl vendored Normal file

Binary file not shown.

BIN
dist/ragnar-1.3.37.tar.gz vendored Normal file

Binary file not shown.

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

24
setup.cfg Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
[console_scripts]
ragnar.run = ragnar.cli:run

View File

@ -0,0 +1,3 @@
aiohttp==3.10.10
dataset==1.6.2
requests==2.32.3

View File

@ -0,0 +1 @@
ragnar

0
src/ragnar/__init__.py Normal file
View File

0
src/ragnar/__main__.py Normal file
View File

96
src/ragnar/api.py Normal file
View File

@ -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)

93
src/ragnar/bot.py Normal file
View File

@ -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))

14
src/ragnar/cache.py Normal file
View File

@ -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

42
src/ragnar/cli.py Normal file
View File

@ -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()