sololegends / runelite-friend-finder

MIT License
0 stars 1 forks source link

Does This Work For Clans? #5

Open Looodon opened 1 month ago

Looodon commented 1 month ago

I'd like to try this with people in my clan without having to add them to my friends list. Is that possible? If not I hope that it would be some day because this is a really cool plugin

sololegends commented 1 month ago

Presently I don't have a plan to open it up to an entire clan. I chose not to do this this far because, at least in my clan, we let a lot of new people in regularly. I'd worry that a new person might join, find out they can see people on the map, and decide to hunt someone in the wildy before we know whether that person can be trusted.

I might add an optional open to clan option, maybe just blank out the wildy for it too as a safety measure.

Technically you could also have a clan server for this and just make no restrictions to who is served locations. You'd need to build a custom server for it instead of using my default one, but the server endpoints are pretty simple especially if removing the security of friend list restrictions.

Looodon commented 1 month ago

I figured the clan function was a safety concern, I figured if someone wants anonymity they can turn off the plugin. It would be nice if you could toggle on/off whether it shows your location in the wild or not because In the clan I'm in everyone's trusted & we host calisto events so It would help us in the wildy. I'll look into the self hosting part, thank you for the response and I'd love to see if you'd explore the clan feature. Much appreciated for the plugin!

EDIT: I don't know much about coding so excuse me if this is wrong, but can I host the server for free on Replit if i set up the API as a secret? The free version does not allow you to make the code private so I made some alterations to hide the API key, added rate-limiter, and tried to make the IP not be stored. Will this work and is it a privacy/security risk? this is the code:

import os
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

# Retrieve the API key from environment variables
API_KEY = os.getenv('API_KEY')

# Initialize rate limiter
limiter = Limiter(
    key_func=lambda: request.headers.get('X-Rate-Limit-Token', 'default'),  # Use custom header for rate limiting
    app=app,
    default_limits=["200 per day", "50 per hour"]
)

# In-memory storage for demonstration purposes
player_data = {}
location_reports = {}

# Helper function to validate player data
def validate_player_data(data):
    required_fields = ['name', 'id', 'x', 'y', 'z', 'w', 'r', 'hm', 'hM', 'pm', 'pM', 'friends']
    for field in required_fields:
        if field not in data:
            return False
    return True

# Endpoint to receive player location data
@app.route('/player-location', methods=['POST'])
@limiter.limit("5 per minute")  # Limit to 5 requests per minute
def receive_player_location():
    api_key = request.headers.get('Authorization')
    if not api_key or api_key != f'Bearer {API_KEY}':
        return jsonify({'error': 'Unauthorized'}), 401

    data = request.json
    if not validate_player_data(data):
        return jsonify({'error': 'Invalid data'}), 400

    player_data[data['id']] = data
    return jsonify({'status': 'received'}), 200

# Endpoint to retrieve player location data
@app.route('/player-location/<int:player_id>', methods=['GET'])
@limiter.limit("10 per minute")  # Limit to 10 requests per minute
def get_player_location(player_id):
    data = player_data.get(player_id)
    if not data:
        return jsonify({'error': 'Not found'}), 404
    return jsonify(data), 200

# Endpoint to receive location reports
@app.route('/location-report', methods=['POST'])
@limiter.limit("5 per minute")  # Limit to 5 requests per minute
def receive_location_report():
    api_key = request.headers.get('Authorization')
    if not api_key or api_key != f'Bearer {API_KEY}':
        return jsonify({'error': 'Unauthorized'}), 401

    data = request.json
    location_reports[data['r']] = data
    return jsonify({'status': 'received'}), 200

# Error handling for internal server errors
@app.errorhandler(500)
def internal_error(error):
    return jsonify({'error': 'Internal server error'}), 500

# Run the app
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)`
sololegends commented 1 month ago

I'll certainly look into clan wide usage, though that would put a greater strain on my server so I'll have to weigh performance costs and additional runtime costs for the server I provide for free to everyone.

At the very least a wildy toggle should be a relatively simple addition, that I'll certainly add in!

As for your API server code there. Storing the API_KEY in the env vars should be fine as far as not showing in the code is generally a good idea. With Replit in particular, I'm not familiar with their service, if the code is always public for free tier, just make sure the env vars are NOT shown to the public as well and security should be good enough.

The Endpoint /player-location looks good for the incoming, however you'll want to reply with the array of locations you with the requesting player to display on their client in the form of the "Player location data RESPONSE payload" as noted in the README. Additionally, 5 per minute would reject a bunch of requests by default, the plugin requests every 2s by default though that can be changed in options. Just make sure you get the client settings for everyone connecting to not get rate limited by your end points.

If you don't implement some location lookup table by region ids or by area coordinates, you'll be limited to only the ones baked into the client. If you like you can pull the full registered location set I've curated from my free API (https://runelite.sololegends.com/locations) and craft a lookup table from that. However that would involve a good deal more code and care in drafting the response data. Eeehhh nvm I more or less just did it below <3.

Implementing /location-report is totally optional so you don't even have to implement that unless you're going to support server based location processing/responses.

TL;DR: I would rework the player-location function as so:

player_data = {}
def receive_player_location():
    api_key = request.headers.get('Authorization')
    if not api_key or api_key != f'Bearer {API_KEY}':
        return jsonify({'error': 'Unauthorized'}), 401

    data = request.json
    if not validate_player_data(data):
        return jsonify({'error': 'Invalid data'}), 400

    player_data[data['id']] = data
    # Craft the response payload
    payload = []
    # NOTE: Better to pre-compute this as players send data in, and just return the pre-computed result each time. 
    # But I'll leave that to you
    for player_id in player_data:
        # Ignore requester data entry
        if player_id == data['id']: 
            continue
        data_ref = player_data[player_id].copy()
        # Remove friends data as that isn't part of the response payload
        del data_ref['friends']
        # Append to the response array
        payload.append(data_ref)

    return jsonify(payload), 200

For a location lookup Location Data

{ 
  "instance":-1,
  "x":0,
  "y":0,
  "location":"Egg Mines",
  "z":0,
  "region":16735
}
# Key region ID >> Value location data
locations = {}
UNKNOWN_REGION = { "instance": -1, "x": 0, "y":0, "location": "Unknown", "z":0 }
def lookup_location(region_id):
    if region_id in locations:
        return location[region_id]
    return UNKNOWN_REGION

# Pre-Process Locations data from my API format, run once on startup if you don't want to mutate it before hand. 
# Or pull direct from public Friend Finder API on startup then mutate
locations_from_sololegends = {...}
def pre_process_locations():
    for location in locations_from_sololegends:
        locations[location.region] = location

Forgive me my python is a tad rusty, I didn't test this code, and it is presently 1am so results may vary lol