commit
c48bf54dc1
22
.gitea/workflows/build.yaml
Normal file
22
.gitea/workflows/build.yaml
Normal 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
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
config.py
|
||||||
|
.venv
|
||||||
|
.history
|
||||||
|
src/ragnar/__pycache__
|
18
Makefile
Normal file
18
Makefile
Normal 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
28
README.md
Normal 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
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
BIN
dist/ragnar-1.3.37.tar.gz
vendored
Normal file
Binary file not shown.
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
24
setup.cfg
Normal file
24
setup.cfg
Normal 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
|
12
src/Ragnar.egg-info/PKG-INFO
Normal file
12
src/Ragnar.egg-info/PKG-INFO
Normal 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
|
14
src/Ragnar.egg-info/SOURCES.txt
Normal file
14
src/Ragnar.egg-info/SOURCES.txt
Normal 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
|
1
src/Ragnar.egg-info/dependency_links.txt
Normal file
1
src/Ragnar.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
2
src/Ragnar.egg-info/entry_points.txt
Normal file
2
src/Ragnar.egg-info/entry_points.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[console_scripts]
|
||||||
|
ragnar.run = ragnar.cli:run
|
3
src/Ragnar.egg-info/requires.txt
Normal file
3
src/Ragnar.egg-info/requires.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
aiohttp==3.10.10
|
||||||
|
dataset==1.6.2
|
||||||
|
requests==2.32.3
|
1
src/Ragnar.egg-info/top_level.txt
Normal file
1
src/Ragnar.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
ragnar
|
0
src/ragnar/__init__.py
Normal file
0
src/ragnar/__init__.py
Normal file
0
src/ragnar/__main__.py
Normal file
0
src/ragnar/__main__.py
Normal file
96
src/ragnar/api.py
Normal file
96
src/ragnar/api.py
Normal 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
93
src/ragnar/bot.py
Normal 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
14
src/ragnar/cache.py
Normal 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
42
src/ragnar/cli.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user