Skip to content

Commit 3572dd7

Browse files
committed
Replace tokens with guest accounts, swap endpoints
1 parent d7ca353 commit 3572dd7

File tree

12 files changed

+159
-382
lines changed

12 files changed

+159
-382
lines changed

nitter.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ requires "https://github.com/zedeus/redis#d0a0e6f"
2323
requires "zippy#ca5989a"
2424
requires "flatty#e668085"
2525
requires "jsony#ea811be"
26-
26+
requires "oauth#b8c163b"
2727

2828
# Tasks
2929

src/api.nim

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,6 @@ proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profi
3333
js = await fetch(url ? params, apiId)
3434
result = parseGraphTimeline(js, "user", after)
3535

36-
# proc getTimeline*(id: string; after=""; replies=false): Future[Profile] {.async.} =
37-
# if id.len == 0: return
38-
# let
39-
# ps = genParams({"userId": id, "include_tweet_replies": $replies}, after)
40-
# url = oldUserTweets / (id & ".json") ? ps
41-
# result = parseTimeline(await fetch(url, Api.timeline), after)
42-
43-
proc getUserTimeline*(id: string; after=""): Future[Profile] {.async.} =
44-
var ps = genParams({"id": id})
45-
if after.len > 0:
46-
ps.add ("down_cursor", after)
47-
48-
let
49-
url = legacyUserTweets ? ps
50-
js = await fetch(url, Api.userTimeline)
51-
result = parseUserTimeline(js, after)
52-
5336
proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
5437
if id.len == 0: return
5538
let
@@ -112,10 +95,10 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
11295
if after.len > 0:
11396
result.replies = await getReplies(id, after)
11497

115-
proc getGraphSearch*(query: Query; after=""): Future[Profile] {.async.} =
98+
proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
11699
let q = genQueryParam(query)
117100
if q.len == 0 or q == emptyQuery:
118-
return Profile(tweets: Timeline(query: query, beginning: true))
101+
return Timeline(query: query, beginning: true)
119102

120103
var
121104
variables = %*{
@@ -129,44 +112,29 @@ proc getGraphSearch*(query: Query; after=""): Future[Profile] {.async.} =
129112
if after.len > 0:
130113
variables["cursor"] = % after
131114
let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures}
132-
result = Profile(tweets: parseGraphSearch(await fetch(url, Api.search), after))
133-
result.tweets.query = query
134-
135-
proc getTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
136-
var q = genQueryParam(query)
137-
138-
if q.len == 0 or q == emptyQuery:
139-
return Timeline(query: query, beginning: true)
140-
141-
if after.len > 0:
142-
q &= " max_id:" & after
143-
144-
let url = tweetSearch ? genParams({
145-
"q": q ,
146-
"modules": "status",
147-
"result_type": "recent",
148-
})
149-
150-
result = parseTweetSearch(await fetch(url, Api.search), after)
115+
result = parseGraphSearch[Tweets](await fetch(url, Api.search), after)
151116
result.query = query
152117

153-
proc getUserSearch*(query: Query; page="1"): Future[Result[User]] {.async.} =
118+
proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.} =
154119
if query.text.len == 0:
155120
return Result[User](query: query, beginning: true)
156121

157-
var url = userSearch ? {
158-
"q": query.text,
159-
"skip_status": "1",
160-
"count": "20",
161-
"page": page
162-
}
122+
var
123+
variables = %*{
124+
"rawQuery": query.text,
125+
"count": 20,
126+
"product": "People",
127+
"withDownvotePerspective": false,
128+
"withReactionsMetadata": false,
129+
"withReactionsPerspective": false
130+
}
131+
if after.len > 0:
132+
variables["cursor"] = % after
133+
result.beginning = false
163134

164-
result = parseUsers(await fetchRaw(url, Api.userSearch))
135+
let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures}
136+
result = parseGraphSearch[User](await fetch(url, Api.search), after)
165137
result.query = query
166-
if page.len == 0:
167-
result.bottom = "2"
168-
elif page.allCharsInSet(Digits):
169-
result.bottom = $(parseInt(page) + 1)
170138

171139
proc getPhotoRail*(name: string): Future[PhotoRail] {.async.} =
172140
if name.len == 0: return

src/apiutils.nim

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPDX-License-Identifier: AGPL-3.0-only
2-
import httpclient, asyncdispatch, options, strutils, uri
3-
import jsony, packedjson, zippy
2+
import httpclient, asyncdispatch, options, strutils, uri, times, math
3+
import jsony, packedjson, zippy, oauth1
44
import types, tokens, consts, parserutils, http_pool
55
import experimental/types/common
66

@@ -29,12 +29,30 @@ proc genParams*(pars: openArray[(string, string)] = @[]; cursor="";
2929
else:
3030
result &= ("cursor", cursor)
3131

32-
proc genHeaders*(token: Token = nil): HttpHeaders =
32+
proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
33+
let
34+
encodedUrl = url.replace(",", "%2C").replace("+", "%20")
35+
params = OAuth1Parameters(
36+
consumerKey: consumerKey,
37+
signatureMethod: "HMAC-SHA1",
38+
timestamp: $int(round(epochTime())),
39+
nonce: "0",
40+
isIncludeVersionToHeader: true,
41+
token: oauthToken
42+
)
43+
signature = getSignature(HttpGet, encodedUrl, "", params, consumerSecret, oauthTokenSecret)
44+
45+
params.signature = percentEncode(signature)
46+
47+
return getOauth1RequestHeader(params)["authorization"]
48+
49+
proc genHeaders*(url, oauthToken, oauthTokenSecret: string): HttpHeaders =
50+
let header = getOauthHeader(url, oauthToken, oauthTokenSecret)
51+
3352
result = newHttpHeaders({
3453
"connection": "keep-alive",
35-
"authorization": auth,
54+
"authorization": header,
3655
"content-type": "application/json",
37-
"x-guest-token": if token == nil: "" else: token.tok,
3856
"x-twitter-active-user": "yes",
3957
"authority": "api.twitter.com",
4058
"accept-encoding": "gzip",
@@ -43,24 +61,24 @@ proc genHeaders*(token: Token = nil): HttpHeaders =
4361
"DNT": "1"
4462
})
4563

46-
template updateToken() =
64+
template updateAccount() =
4765
if resp.headers.hasKey(rlRemaining):
4866
let
4967
remaining = parseInt(resp.headers[rlRemaining])
5068
reset = parseInt(resp.headers[rlReset])
51-
token.setRateLimit(api, remaining, reset)
69+
account.setRateLimit(api, remaining, reset)
5270

5371
template fetchImpl(result, fetchBody) {.dirty.} =
5472
once:
5573
pool = HttpPool()
5674

57-
var token = await getToken(api)
58-
if token.tok.len == 0:
75+
var account = await getGuestAccount(api)
76+
if account.oauthToken.len == 0:
5977
raise rateLimitError()
6078

6179
try:
6280
var resp: AsyncResponse
63-
pool.use(genHeaders(token)):
81+
pool.use(genHeaders($url, account.oauthToken, account.oauthSecret)):
6482
template getContent =
6583
resp = await c.get($url)
6684
result = await resp.body
@@ -79,19 +97,19 @@ template fetchImpl(result, fetchBody) {.dirty.} =
7997

8098
fetchBody
8199

82-
release(token, used=true)
100+
release(account, used=true)
83101

84102
if resp.status == $Http400:
85103
raise newException(InternalError, $url)
86104
except InternalError as e:
87105
raise e
88106
except BadClientError as e:
89-
release(token, used=true)
107+
release(account, used=true)
90108
raise e
91109
except Exception as e:
92-
echo "error: ", e.name, ", msg: ", e.msg, ", token: ", token[], ", url: ", url
110+
echo "error: ", e.name, ", msg: ", e.msg, ", accountId: ", account.id, ", url: ", url
93111
if "length" notin e.msg and "descriptor" notin e.msg:
94-
release(token, invalid=true)
112+
release(account, invalid=true)
95113
raise rateLimitError()
96114

97115
proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
@@ -103,12 +121,12 @@ proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} =
103121
echo resp.status, ": ", body, " --- url: ", url
104122
result = newJNull()
105123

106-
updateToken()
124+
updateAccount()
107125

108126
let error = result.getError
109127
if error in {invalidToken, badToken}:
110128
echo "fetch error: ", result.getError
111-
release(token, invalid=true)
129+
release(account, invalid=true)
112130
raise rateLimitError()
113131

114132
proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} =
@@ -117,11 +135,11 @@ proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} =
117135
echo resp.status, ": ", result, " --- url: ", url
118136
result.setLen(0)
119137

120-
updateToken()
138+
updateAccount()
121139

122140
if result.startsWith("{\"errors"):
123141
let errors = result.fromJson(Errors)
124142
if errors in {invalidToken, badToken}:
125143
echo "fetch error: ", errors
126-
release(token, invalid=true)
144+
release(account, invalid=true)
127145
raise rateLimitError()

src/consts.nim

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,13 @@
22
import uri, sequtils, strutils
33

44
const
5-
auth* = "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF"
5+
consumerKey* = "3nVuSoBZnx6U4vzUxf5w"
6+
consumerSecret* = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"
67

78
api = parseUri("https://api.twitter.com")
89
activate* = $(api / "1.1/guest/activate.json")
910

10-
legacyUserTweets* = api / "1.1/timeline/user.json"
1111
photoRail* = api / "1.1/statuses/media_timeline.json"
12-
userSearch* = api / "1.1/users/search.json"
13-
tweetSearch* = api / "1.1/search/universal.json"
14-
15-
# oldUserTweets* = api / "2/timeline/profile"
1612

1713
graphql = api / "graphql"
1814
graphUser* = graphql / "u7wQyGi6oExe8_TRWGMq4Q/UserResultByScreenNameQuery"

src/nitter.nim

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import asyncdispatch, strformat, logging
33
from net import Port
44
from htmlgen import a
55
from os import getEnv
6+
from json import parseJson
67

78
import jester
89

@@ -15,8 +16,14 @@ import routes/[
1516
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
1617
const issuesUrl = "https://github.com/zedeus/nitter/issues"
1718

18-
let configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf")
19-
let (cfg, fullCfg) = getConfig(configPath)
19+
let
20+
configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf")
21+
(cfg, fullCfg) = getConfig(configPath)
22+
23+
accountsPath = getEnv("NITTER_ACCOUNTS_FILE", "./guest_accounts.json")
24+
accounts = parseJson(readFile(accountsPath))
25+
26+
initAccountPool(cfg, parseJson(readFile(accountsPath)))
2027

2128
if not cfg.enableDebug:
2229
# Silence Jester's query warning
@@ -38,8 +45,6 @@ waitFor initRedisPool(cfg)
3845
stdout.write &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}\n"
3946
stdout.flushFile
4047

41-
asyncCheck initTokenPool(cfg)
42-
4348
createUnsupportedRouter(cfg)
4449
createResolverRouter(cfg)
4550
createPrefRouter(cfg)

0 commit comments

Comments
 (0)