|
| 1 | +# ------------------------------------------------------------------------------- |
| 2 | +# Name: sfp_xquik |
| 3 | +# Purpose: Query Xquik for X/Twitter profile information. |
| 4 | +# |
| 5 | +# Author: SpiderFoot contributors |
| 6 | +# |
| 7 | +# Created: 2026-06-13 |
| 8 | +# Copyright: (c) SpiderFoot contributors 2026 |
| 9 | +# Licence: MIT |
| 10 | +# ------------------------------------------------------------------------------- |
| 11 | + |
| 12 | +import json |
| 13 | +import re |
| 14 | +import urllib.parse |
| 15 | + |
| 16 | +from spiderfoot import SpiderFootEvent, SpiderFootPlugin |
| 17 | + |
| 18 | + |
| 19 | +class sfp_xquik(SpiderFootPlugin): |
| 20 | + |
| 21 | + meta = { |
| 22 | + 'name': "Xquik", |
| 23 | + 'summary': "Gather X/Twitter user profile details from Xquik.", |
| 24 | + 'flags': ["apikey"], |
| 25 | + 'useCases': ["Footprint", "Investigate", "Passive"], |
| 26 | + 'categories': ["Social Media"], |
| 27 | + 'dataSource': { |
| 28 | + 'website': "https://xquik.com/", |
| 29 | + 'model': "FREE_AUTH_LIMITED", |
| 30 | + 'references': [ |
| 31 | + "https://docs.xquik.com/api-reference/overview" |
| 32 | + ], |
| 33 | + 'apiKeyInstructions': [ |
| 34 | + "Visit https://xquik.com/", |
| 35 | + "Create or sign in to your account", |
| 36 | + "Open API keys from the dashboard", |
| 37 | + "Create an API key and paste it into the module options" |
| 38 | + ], |
| 39 | + 'favIcon': "https://xquik.com/icon.svg", |
| 40 | + 'logo': "https://xquik.com/icon.svg", |
| 41 | + 'description': "Xquik provides X/Twitter data APIs for profile lookup, " |
| 42 | + "tweet search, media download, monitoring, and automation workflows.", |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + opts = { |
| 47 | + "api_key": "", |
| 48 | + "base_url": "https://xquik.com/api/v1" |
| 49 | + } |
| 50 | + |
| 51 | + optdescs = { |
| 52 | + "api_key": "Xquik API key.", |
| 53 | + "base_url": "Xquik API base URL." |
| 54 | + } |
| 55 | + |
| 56 | + handle_re = re.compile(r"^[A-Za-z0-9_]{1,15}$") |
| 57 | + |
| 58 | + results = None |
| 59 | + errorState = False |
| 60 | + |
| 61 | + def setup(self, sfc, userOpts=dict()): |
| 62 | + self.sf = sfc |
| 63 | + self.__dataSource__ = "Xquik" |
| 64 | + self.results = self.tempStorage() |
| 65 | + self.errorState = False |
| 66 | + |
| 67 | + for opt in list(userOpts.keys()): |
| 68 | + self.opts[opt] = userOpts[opt] |
| 69 | + |
| 70 | + def watchedEvents(self): |
| 71 | + return ["SOCIAL_MEDIA"] |
| 72 | + |
| 73 | + def producedEvents(self): |
| 74 | + return ["RAW_RIR_DATA", "USERNAME"] |
| 75 | + |
| 76 | + def extract_handle(self, event_data): |
| 77 | + try: |
| 78 | + network, value = event_data.split(": ", 1) |
| 79 | + except ValueError: |
| 80 | + return None |
| 81 | + |
| 82 | + if network.lower() not in ["twitter", "x"]: |
| 83 | + return None |
| 84 | + |
| 85 | + url = value.replace("<SFURL>", "").replace("</SFURL>", "").strip() |
| 86 | + |
| 87 | + try: |
| 88 | + parsed = urllib.parse.urlparse(url) |
| 89 | + except Exception: |
| 90 | + return None |
| 91 | + |
| 92 | + if parsed.scheme not in ["http", "https"]: |
| 93 | + return None |
| 94 | + |
| 95 | + if parsed.netloc.lower() not in ["twitter.com", "www.twitter.com", "x.com", "www.x.com"]: |
| 96 | + return None |
| 97 | + |
| 98 | + parts = [part for part in parsed.path.split("/") if part] |
| 99 | + if len(parts) != 1: |
| 100 | + return None |
| 101 | + |
| 102 | + handle = parts[0].lstrip("@") |
| 103 | + if not self.handle_re.match(handle): |
| 104 | + return None |
| 105 | + |
| 106 | + if handle.lower() in ["home", "explore", "i", "intent", "messages", "notifications", "search"]: |
| 107 | + return None |
| 108 | + |
| 109 | + return handle |
| 110 | + |
| 111 | + def query(self, handle): |
| 112 | + base_url = self.opts["base_url"].rstrip("/") |
| 113 | + user = urllib.parse.quote(handle, safe="") |
| 114 | + headers = { |
| 115 | + "x-api-key": self.opts["api_key"] |
| 116 | + } |
| 117 | + |
| 118 | + return self.sf.fetchUrl(f"{base_url}/x/users/{user}", |
| 119 | + timeout=self.opts.get("_fetchtimeout", 30), |
| 120 | + useragent="SpiderFoot", |
| 121 | + headers=headers) |
| 122 | + |
| 123 | + def handleEvent(self, event): |
| 124 | + eventData = event.data |
| 125 | + |
| 126 | + if self.errorState: |
| 127 | + return |
| 128 | + |
| 129 | + handle = self.extract_handle(eventData) |
| 130 | + if handle is None: |
| 131 | + return |
| 132 | + |
| 133 | + lookup_key = handle.lower() |
| 134 | + if lookup_key in self.results: |
| 135 | + return |
| 136 | + |
| 137 | + self.results[lookup_key] = True |
| 138 | + |
| 139 | + if self.opts["api_key"] == "": |
| 140 | + self.error("You enabled sfp_xquik but did not set an API key!") |
| 141 | + self.errorState = True |
| 142 | + return |
| 143 | + |
| 144 | + res = self.query(handle) |
| 145 | + if not res or not res.get("content"): |
| 146 | + return |
| 147 | + |
| 148 | + code = str(res.get("code")) |
| 149 | + if code in ["401", "402", "403"]: |
| 150 | + self.error(f"Xquik API request failed with HTTP {code}.") |
| 151 | + self.errorState = True |
| 152 | + return |
| 153 | + |
| 154 | + if code != "200": |
| 155 | + self.debug(f"Xquik API request failed with HTTP {code}.") |
| 156 | + return |
| 157 | + |
| 158 | + try: |
| 159 | + data = json.loads(res["content"]) |
| 160 | + except Exception as e: |
| 161 | + self.error(f"Error processing JSON response from Xquik: {e}") |
| 162 | + return |
| 163 | + |
| 164 | + evt = SpiderFootEvent("RAW_RIR_DATA", json.dumps(data, sort_keys=True), |
| 165 | + self.__name__, event) |
| 166 | + self.notifyListeners(evt) |
| 167 | + |
| 168 | + payload = data.get("data", data) |
| 169 | + if not isinstance(payload, dict): |
| 170 | + return |
| 171 | + |
| 172 | + username = payload.get("username") or payload.get("screen_name") or payload.get("handle") |
| 173 | + if not username or not isinstance(username, str): |
| 174 | + return |
| 175 | + |
| 176 | + username = username.lstrip("@") |
| 177 | + if not self.handle_re.match(username): |
| 178 | + return |
| 179 | + |
| 180 | + evt = SpiderFootEvent("USERNAME", username, self.__name__, event) |
| 181 | + self.notifyListeners(evt) |
| 182 | + |
| 183 | +# End of sfp_xquik class |
0 commit comments