Initial commit
All checks were successful
Build Ragnar anti spam bot / Build (push) Successful in 1m31s
All checks were successful
Build Ragnar anti spam bot / Build (push) Successful in 1m31s
This commit is contained in:
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