mirror of
https://github.com/Viren070/docker-compose-template.git
synced 2025-12-01 23:17:00 +01:00
feat: add nitter
This commit is contained in:
@@ -142,6 +142,7 @@ LIBRESPEED_HOSTNAME=speedtest.${DOMAIN}
|
||||
MEDIAFLOW_PROXY_HOSTNAME=mediaflow-proxy.${DOMAIN}
|
||||
MEDIAFUSION_HOSTNAME=mediafusion.${DOMAIN}
|
||||
MINECRAFT_HOSTNAME=mc.${DOMAIN}
|
||||
NITTER_HOSTNAME=nitter.${DOMAIN}
|
||||
NZBHYDRA2_HOSTNAME=nzbhydra2.${DOMAIN}
|
||||
OMG_TV_STREMIO_ADDON_HOSTNAME=omg-tv-addon.${DOMAIN}
|
||||
OVERSEERR_HOSTNAME=overseerr.${DOMAIN}
|
||||
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
${MEDIAFLOW_PROXY_HOSTNAME},
|
||||
${MEDIAFUSION_HOSTNAME},
|
||||
${MINECRAFT_HOSTNAME},
|
||||
${NITTER_HOSTNAME},
|
||||
${NZBHYDRA2_HOSTNAME},
|
||||
${OMG_TV_STREMIO_ADDON_HOSTNAME},
|
||||
${OVERSEERR_HOSTNAME},
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# The tag to use for the Nitter Docker image.
|
||||
# If not on an arm64 architecture, you can use the latest tag.
|
||||
NITTER_TAG=latest-arm64
|
||||
|
||||
# In order to run Nitter, we need to obtain the session secrets from Twitter.
|
||||
# To do this, fill in the following values with your Twitter credentials:
|
||||
# NOTE: Please remove the username and password from here after running the containers once successfully.
|
||||
# You will find the sessions in the ./sessions/sessions.jsonl file to see if it worked, you can also check the logs of the nitter_sessions container.
|
||||
TW_USERNAME=
|
||||
TW_PASSWORD=
|
||||
TW_OTP_SECRET=
|
||||
|
||||
# Make sure to also edit the nitter.conf file to set the hostname to your own domain or IP address.
|
||||
# And provide the hmacKey in the nitter.conf file, which is used for cryptographic signing of video URLs.
|
||||
# You can generate a random key using the following command:
|
||||
# openssl rand -base64 42 | tr -dc A-Za-z0-9 | cut -c -32 | tr -d '\n'; echo
|
||||
@@ -0,0 +1,64 @@
|
||||
services:
|
||||
nitter:
|
||||
image: zedeus/nitter:${NITTER_TAG:-latest}
|
||||
container_name: nitter
|
||||
restart: unless-stopped
|
||||
user: ${PUID}:${PGID}
|
||||
expose:
|
||||
- 8080
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.nitter.rule=Host(`${NITTER_HOSTNAME?}`)"
|
||||
- "traefik.http.routers.nitter.entrypoints=websecure"
|
||||
- "traefik.http.routers.nitter.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.nitter.loadbalancer.server.port=8080"
|
||||
- "traefik.http.routers.nitter.middlewares=authelia@docker"
|
||||
volumes:
|
||||
- ./nitter.conf:/src/nitter.conf:Z,ro
|
||||
- ./sessions/sessions.jsonl:/src/sessions.jsonl:Z,ro
|
||||
depends_on:
|
||||
nitter_redis:
|
||||
condition: service_healthy
|
||||
nitter_sessions:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:8080/Jack/status/20 || exit 1
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 10s
|
||||
retries: 2
|
||||
profiles:
|
||||
- nitter
|
||||
- all
|
||||
|
||||
nitter_redis:
|
||||
image: redis:6-alpine
|
||||
container_name: nitter_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- ${DOCKER_DATA_DIR}/nitter:/data
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
profiles:
|
||||
- nitter
|
||||
- all
|
||||
|
||||
nitter_sessions:
|
||||
build:
|
||||
context: ./sessions
|
||||
dockerfile: Dockerfile
|
||||
container_name: nitter_sessions
|
||||
restart: no
|
||||
environment:
|
||||
USERNAME: ${TW_USERNAME}
|
||||
PASSWORD: ${TW_PASSWORD}
|
||||
OTP_SECRET: ${TW_OTP_SECRET}
|
||||
volumes:
|
||||
- ./sessions:/app/sessions
|
||||
profiles:
|
||||
- nitter
|
||||
- all
|
||||
@@ -0,0 +1,38 @@
|
||||
[Server]
|
||||
hostname = "nitter.net" # for generating links, change this to your own domain/ip
|
||||
title = "nitter"
|
||||
address = "0.0.0.0"
|
||||
port = 8080
|
||||
https = false # disable to enable cookies when not using https
|
||||
httpMaxConnections = 100
|
||||
staticDir = "./public"
|
||||
|
||||
[Cache]
|
||||
listMinutes = 240 # how long to cache list info (not the tweets, so keep it high)
|
||||
rssMinutes = 10 # how long to cache rss queries
|
||||
redisHost = "nitter_redis"
|
||||
redisPort = 6379
|
||||
redisPassword = ""
|
||||
redisConnections = 20 # minimum open connections in pool
|
||||
redisMaxConnections = 30
|
||||
# new connections are opened when none are available, but if the pool size
|
||||
# goes above this, they're closed when released. don't worry about this unless
|
||||
# you receive tons of requests per second
|
||||
|
||||
[Config]
|
||||
hmacKey = "80c6b3849c8f6e3013986eeecc1e70b06a1ef4e2168a37cb5b7b4385d241e13e" # random key for cryptographic signing of video urls
|
||||
base64Media = false # use base64 encoding for proxied media urls
|
||||
enableRSS = true # set this to false to disable RSS feeds
|
||||
enableDebug = true # enable request logs and debug endpoints (/.sessions)
|
||||
proxy = "http://warp:1080" # http/https url, SOCKS proxies are not supported
|
||||
proxyAuth = ""
|
||||
|
||||
# Change default preferences here, see src/prefs_impl.nim for a complete list
|
||||
[Preferences]
|
||||
theme = "Nitter"
|
||||
replaceTwitter = "nitter.net"
|
||||
replaceYouTube = "piped.video"
|
||||
replaceReddit = "teddit.net"
|
||||
proxyVideos = true
|
||||
hlsPlayback = false
|
||||
infiniteScroll = false
|
||||
@@ -0,0 +1,14 @@
|
||||
# Use a lightweight Python base image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir requests pyotp
|
||||
|
||||
# Copy the script into the container
|
||||
COPY get_session.py .
|
||||
|
||||
# Default command (can be overridden)
|
||||
ENTRYPOINT ["python3", "get_session.py"]
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import pyotp
|
||||
import os
|
||||
|
||||
# NOTE: pyotp and requests are dependencies
|
||||
# > pip install pyotp requests
|
||||
|
||||
TW_CONSUMER_KEY = '3nVuSoBZnx6U4vzUxf5w'
|
||||
TW_CONSUMER_SECRET = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys'
|
||||
|
||||
def auth(username, password, otp_secret):
|
||||
bearer_token_req = requests.post("https://api.twitter.com/oauth2/token",
|
||||
auth=(TW_CONSUMER_KEY, TW_CONSUMER_SECRET),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data='grant_type=client_credentials'
|
||||
).json()
|
||||
bearer_token = ' '.join(str(x) for x in bearer_token_req.values())
|
||||
|
||||
guest_token = requests.post(
|
||||
"https://api.twitter.com/1.1/guest/activate.json",
|
||||
headers={'Authorization': bearer_token}
|
||||
).json().get('guest_token')
|
||||
|
||||
if not guest_token:
|
||||
print("Failed to obtain guest token.")
|
||||
sys.exit(1)
|
||||
|
||||
twitter_header = {
|
||||
'Authorization': bearer_token,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "TwitterAndroid/10.21.0-release.0 (310210000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)",
|
||||
"X-Twitter-API-Version": '5',
|
||||
"X-Twitter-Client": "TwitterAndroid",
|
||||
"X-Twitter-Client-Version": "10.21.0-release.0",
|
||||
"OS-Version": "28",
|
||||
"System-User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)",
|
||||
"X-Twitter-Active-User": "yes",
|
||||
"X-Guest-Token": guest_token,
|
||||
"X-Twitter-Client-DeviceID": ""
|
||||
}
|
||||
|
||||
session = requests.Session()
|
||||
session.headers = twitter_header
|
||||
|
||||
task1 = session.post(
|
||||
'https://api.twitter.com/1.1/onboarding/task.json',
|
||||
params={
|
||||
'flow_name': 'login',
|
||||
'api_version': '1',
|
||||
'known_device_token': '',
|
||||
'sim_country_code': 'us'
|
||||
},
|
||||
json={
|
||||
"flow_token": None,
|
||||
"input_flow_data": {
|
||||
"country_code": None,
|
||||
"flow_context": {
|
||||
"referrer_context": {
|
||||
"referral_details": "utm_source=google-play&utm_medium=organic",
|
||||
"referrer_url": ""
|
||||
},
|
||||
"start_location": {
|
||||
"location": "deeplink"
|
||||
}
|
||||
},
|
||||
"requested_variant": None,
|
||||
"target_user_id": 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
session.headers['att'] = task1.headers.get('att')
|
||||
|
||||
task2 = session.post(
|
||||
'https://api.twitter.com/1.1/onboarding/task.json',
|
||||
json={
|
||||
"flow_token": task1.json().get('flow_token'),
|
||||
"subtask_inputs": [{
|
||||
"enter_text": {
|
||||
"suggestion_id": None,
|
||||
"text": username,
|
||||
"link": "next_link"
|
||||
},
|
||||
"subtask_id": "LoginEnterUserIdentifier"
|
||||
}]
|
||||
}
|
||||
)
|
||||
|
||||
task3 = session.post(
|
||||
'https://api.twitter.com/1.1/onboarding/task.json',
|
||||
json={
|
||||
"flow_token": task2.json().get('flow_token'),
|
||||
"subtask_inputs": [{
|
||||
"enter_password": {
|
||||
"password": password,
|
||||
"link": "next_link"
|
||||
},
|
||||
"subtask_id": "LoginEnterPassword"
|
||||
}],
|
||||
}
|
||||
)
|
||||
|
||||
for t3_subtask in task3.json().get('subtasks', []):
|
||||
if "open_account" in t3_subtask:
|
||||
return t3_subtask["open_account"]
|
||||
elif "enter_text" in t3_subtask:
|
||||
response_text = t3_subtask["enter_text"]["hint_text"]
|
||||
totp = pyotp.TOTP(otp_secret)
|
||||
generated_code = totp.now()
|
||||
task4resp = session.post(
|
||||
"https://api.twitter.com/1.1/onboarding/task.json",
|
||||
json={
|
||||
"flow_token": task3.json().get("flow_token"),
|
||||
"subtask_inputs": [
|
||||
{
|
||||
"enter_text": {
|
||||
"suggestion_id": None,
|
||||
"text": generated_code,
|
||||
"link": "next_link",
|
||||
},
|
||||
"subtask_id": "LoginTwoFactorAuthChallenge",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
task4 = task4resp.json()
|
||||
for t4_subtask in task4.get("subtasks", []):
|
||||
if "open_account" in t4_subtask:
|
||||
return t4_subtask["open_account"]
|
||||
|
||||
return None
|
||||
|
||||
if __name__ == "__main__":
|
||||
username = os.getenv("USERNAME")
|
||||
password = os.getenv("PASSWORD")
|
||||
otp_secret = os.getenv("OTP_SECRET")
|
||||
path = os.getenv("SESSION_PATH", "/app/sessions/sessions.jsonl")
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
if not username or not password:
|
||||
# parse session file
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
sessions = [json.loads(line) for line in f if line.strip()]
|
||||
# if no sessions found, exit
|
||||
if not sessions:
|
||||
print("No sessions found. Please provide username and password.")
|
||||
sys.exit(1)
|
||||
# exit with 0 status code
|
||||
print("No username or password provided. Using existing sessions.")
|
||||
sys.exit(0)
|
||||
except FileNotFoundError:
|
||||
print("Session file not found. Please provide username and password.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
result = auth(username, password, otp_secret)
|
||||
if result is None:
|
||||
print("Authentication failed.")
|
||||
sys.exit(1)
|
||||
|
||||
session_entry = {
|
||||
"oauth_token": result.get("oauth_token"),
|
||||
"oauth_token_secret": result.get("oauth_token_secret")
|
||||
}
|
||||
|
||||
try:
|
||||
with open(path, "a") as f:
|
||||
f.write(json.dumps(session_entry) + "\n")
|
||||
print("Authentication successful. Session appended to", path)
|
||||
except Exception as e:
|
||||
print(f"Failed to write session information: {e}")
|
||||
sys.exit(1)
|
||||
@@ -31,6 +31,7 @@ include:
|
||||
- apps/mediaflow-proxy/compose.yaml
|
||||
- apps/mediafusion/compose.yaml
|
||||
- apps/minecraft/compose.yaml
|
||||
- apps/nitter/compose.yaml
|
||||
- apps/nzbhydra2/compose.yaml
|
||||
- apps/omg-tv-addon/compose.yaml
|
||||
- apps/overseerr/compose.yaml
|
||||
|
||||
Reference in New Issue
Block a user