refactor: added standard nim logging library

This commit is contained in:
2025-11-18 21:24:17 -03:00
parent 4df434a7c6
commit 3845fb1213
9 changed files with 90 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import httpclient, asyncdispatch, options, strutils, uri, times, math, tables import httpclient, asyncdispatch, options, strutils, uri, times, math, tables, logging
import jsony, packedjson, zippy, oauth1 import jsony, packedjson, zippy, oauth1
import types, auth, consts, parserutils, http_pool import types, auth, consts, parserutils, http_pool
import experimental/types/common import experimental/types/common
@@ -60,11 +60,11 @@ proc getAndValidateSession*(api: Api): Future[Session] {.async.} =
case result.kind case result.kind
of SessionKind.oauth: of SessionKind.oauth:
if result.oauthToken.len == 0: if result.oauthToken.len == 0:
echo "[sessions] Empty oauth token, session: ", result.pretty warn "[sessions] Empty oauth token, session: ", result.pretty
raise rateLimitError() raise rateLimitError()
of SessionKind.cookie: of SessionKind.cookie:
if result.authToken.len == 0 or result.ct0.len == 0: if result.authToken.len == 0 or result.ct0.len == 0:
echo "[sessions] Empty cookie credentials, session: ", result.pretty warn "[sessions] Empty cookie credentials, session: ", result.pretty
raise rateLimitError() raise rateLimitError()
template fetchImpl(result, fetchBody) {.dirty.} = template fetchImpl(result, fetchBody) {.dirty.} =
@@ -98,7 +98,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
if result.startsWith("{\"errors"): if result.startsWith("{\"errors"):
let errors = result.fromJson(Errors) let errors = result.fromJson(Errors)
if errors notin errorsToSkip: if errors notin errorsToSkip:
echo "Fetch error, API: ", api, ", errors: ", errors error "Fetch error, API: ", api, ", errors: ", errors
if errors in {expiredToken, badToken, locked}: if errors in {expiredToken, badToken, locked}:
invalidate(session) invalidate(session)
raise rateLimitError() raise rateLimitError()
@@ -107,7 +107,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
setLimited(session, api) setLimited(session, api)
raise rateLimitError() raise rateLimitError()
elif result.startsWith("429 Too Many Requests"): elif result.startsWith("429 Too Many Requests"):
echo "[sessions] 429 error, API: ", api, ", session: ", session.pretty warn "[sessions] 429 error, API: ", api, ", session: ", session.pretty
session.apis[api].remaining = 0 session.apis[api].remaining = 0
# rate limit hit, resets after the 15 minute window # rate limit hit, resets after the 15 minute window
raise rateLimitError() raise rateLimitError()
@@ -115,7 +115,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
fetchBody fetchBody
if resp.status == $Http400: if resp.status == $Http400:
echo "ERROR 400, ", api, ": ", result error "ERROR 400, ", api, ": ", result
raise newException(InternalError, $url) raise newException(InternalError, $url)
except InternalError as e: except InternalError as e:
raise e raise e
@@ -125,7 +125,10 @@ template fetchImpl(result, fetchBody) {.dirty.} =
raise e raise e
except Exception as e: except Exception as e:
let s = session.pretty let s = session.pretty
echo "error: ", e.name, ", msg: ", e.msg, ", session: ", s, ", url: ", url var safeUrl = $url
if safeUrl.len > 100:
safeUrl = safeUrl[0 .. 100] & "..."
error "error: ", e.name, ", msg: ", e.msg, ", session: ", s, ", url: ", safeUrl
raise rateLimitError() raise rateLimitError()
finally: finally:
release(session) release(session)
@@ -134,7 +137,7 @@ template retry(bod) =
try: try:
bod bod
except RateLimitError: except RateLimitError:
echo "[sessions] Rate limited, retrying ", api, " request..." info "[sessions] Rate limited, retrying ", api, " request..."
bod bod
proc fetch*(url: Uri | SessionAwareUrl; api: Api): Future[JsonNode] {.async.} = proc fetch*(url: Uri | SessionAwareUrl; api: Api): Future[JsonNode] {.async.} =
@@ -152,13 +155,13 @@ proc fetch*(url: Uri | SessionAwareUrl; api: Api): Future[JsonNode] {.async.} =
if body.startsWith('{') or body.startsWith('['): if body.startsWith('{') or body.startsWith('['):
result = parseJson(body) result = parseJson(body)
else: else:
echo resp.status, ": ", body, " --- url: ", url warn resp.status, ": ", body, " --- url: ", url
result = newJNull() result = newJNull()
let error = result.getError let apiErr = result.getError
if error != null and error notin errorsToSkip: if apiErr != null and apiErr notin errorsToSkip:
echo "Fetch error, API: ", api, ", error: ", error error "Fetch error, API: ", api, ", error: ", apiErr
if error in {expiredToken, badToken, locked}: if apiErr in {expiredToken, badToken, locked}:
invalidate(session) invalidate(session)
raise rateLimitError() raise rateLimitError()
@@ -173,5 +176,5 @@ proc fetchRaw*(url: Uri | SessionAwareUrl; api: Api): Future[string] {.async.} =
fetchImpl result: fetchImpl result:
if not (result.startsWith('{') or result.startsWith('[')): if not (result.startsWith('{') or result.startsWith('[')):
echo resp.status, ": ", result, " --- url: ", url warn resp.status, ": ", result, " --- url: ", url
result.setLen(0) result.setLen(0)

View File

@@ -1,5 +1,5 @@
#SPDX-License-Identifier: AGPL-3.0-only #SPDX-License-Identifier: AGPL-3.0-only
import std/[asyncdispatch, times, json, random, sequtils, strutils, tables, packedsets, os] import std/[asyncdispatch, times, json, random, strutils, tables, packedsets, os, logging]
import types import types
import experimental/parser/session import experimental/parser/session
@@ -12,8 +12,11 @@ var
sessionPool: seq[Session] sessionPool: seq[Session]
enableLogging = false enableLogging = false
template log(str: varargs[string, `$`]) = proc logSession(args: varargs[string, `$`]) =
echo "[sessions] ", str.join("") var s = "[sessions] "
for arg in args:
s.add arg
info s
proc pretty*(session: Session): string = proc pretty*(session: Session): string =
if session.isNil: if session.isNil:
@@ -129,7 +132,7 @@ proc isLimited(session: Session; api: Api): bool =
if session.limited and api != Api.userTweets: if session.limited and api != Api.userTweets:
if (epochTime().int - session.limitedAt) > hourInSeconds: if (epochTime().int - session.limitedAt) > hourInSeconds:
session.limited = false session.limited = false
log "resetting limit: ", session.pretty logSession "resetting limit: ", session.pretty
return false return false
else: else:
return true return true
@@ -145,7 +148,7 @@ proc isReady(session: Session; api: Api): bool =
proc invalidate*(session: var Session) = proc invalidate*(session: var Session) =
if session.isNil: return if session.isNil: return
log "invalidating: ", session.pretty logSession "invalidating: ", session.pretty
# TODO: This isn't sufficient, but it works for now # TODO: This isn't sufficient, but it works for now
let idx = sessionPool.find(session) let idx = sessionPool.find(session)
@@ -164,13 +167,13 @@ proc getSession*(api: Api): Future[Session] {.async.} =
if not result.isNil and result.isReady(api): if not result.isNil and result.isReady(api):
inc result.pending inc result.pending
else: else:
log "no sessions available for API: ", api logSession "no sessions available for API: ", api
raise noSessionsError() raise noSessionsError()
proc setLimited*(session: Session; api: Api) = proc setLimited*(session: Session; api: Api) =
session.limited = true session.limited = true
session.limitedAt = epochTime().int session.limitedAt = epochTime().int
log "rate limited by api: ", api, ", reqs left: ", session.apis[api].remaining, ", ", session.pretty logSession "rate limited by api: ", api, ", reqs left: ", session.apis[api].remaining, ", ", session.pretty
proc setRateLimit*(session: Session; api: Api; remaining, reset, limit: int) = proc setRateLimit*(session: Session; api: Api; remaining, reset, limit: int) =
# avoid undefined behavior in race conditions # avoid undefined behavior in race conditions
@@ -188,15 +191,15 @@ proc initSessionPool*(cfg: Config; path: string) =
enableLogging = cfg.enableDebug enableLogging = cfg.enableDebug
if path.endsWith(".json"): if path.endsWith(".json"):
log "ERROR: .json is not supported, the file must be a valid JSONL file ending in .jsonl" fatal ".json is not supported, the file must be a valid JSONL file ending in .jsonl"
quit 1 quit 1
if not fileExists(path): if not fileExists(path):
log "ERROR: ", path, " not found. This file is required to authenticate API requests." fatal path, " not found. This file is required to authenticate API requests."
quit 1 quit 1
log "parsing JSONL account sessions file: ", path logSession "parsing JSONL account sessions file: ", path
for line in path.lines: for line in path.lines:
sessionPool.add parseSession(line) sessionPool.add parseSession(line)
log "successfully added ", sessionPool.len, " valid account sessions" logSession "successfully added ", sessionPool.len, " valid account sessions"

View File

@@ -1,4 +1,6 @@
import std/[options, tables, strutils, strformat, sugar]
import std/[options, tables, strutils, strformat, sugar, logging]
import jsony import jsony
import user, ../types/unifiedcard import user, ../types/unifiedcard
import ../../formatters import ../../formatters
@@ -112,7 +114,7 @@ proc parseUnifiedCard*(json: string): Card =
of ComponentType.hidden: of ComponentType.hidden:
result.kind = CardKind.hidden result.kind = CardKind.hidden
of ComponentType.unknown: of ComponentType.unknown:
echo "ERROR: Unknown component type: ", json error "ERROR: Unknown component type: ", json
case component.kind case component.kind
of twitterListDetails: of twitterListDetails:

View File

@@ -1,4 +1,6 @@
import std/[options, tables, times]
import std/[options, tables, times, logging]
import jsony import jsony
from ../../types import VideoType, VideoVariant, User from ../../types import VideoType, VideoVariant, User
@@ -103,21 +105,21 @@ proc enumHook*(s: string; v: var ComponentType) =
of "media_with_details_horizontal": mediaWithDetailsHorizontal of "media_with_details_horizontal": mediaWithDetailsHorizontal
of "commerce_drop_details": hidden of "commerce_drop_details": hidden
of "grok_share": grokShare of "grok_share": grokShare
else: echo "ERROR: Unknown enum value (ComponentType): ", s; unknown else: error "ERROR: Unknown enum value (ComponentType): ", s; unknown
proc enumHook*(s: string; v: var AppType) = proc enumHook*(s: string; v: var AppType) =
v = case s v = case s
of "android_app": androidApp of "android_app": androidApp
of "iphone_app": iPhoneApp of "iphone_app": iPhoneApp
of "ipad_app": iPadApp of "ipad_app": iPadApp
else: echo "ERROR: Unknown enum value (AppType): ", s; androidApp else: error "ERROR: Unknown enum value (AppType): ", s; androidApp
proc enumHook*(s: string; v: var MediaType) = proc enumHook*(s: string; v: var MediaType) =
v = case s v = case s
of "video": video of "video": video
of "photo": photo of "photo": photo
of "model3d": model3d of "model3d": model3d
else: echo "ERROR: Unknown enum value (MediaType): ", s; photo else: error "ERROR: Unknown enum value (MediaType): ", s; photo
proc parseHook*(s: string; i: var int; v: var DateTime) = proc parseHook*(s: string; i: var int; v: var DateTime) =
var str: string var str: string

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, strformat, logging import asyncdispatch, strformat, logging, terminal, times, strutils
from net import Port from net import Port
from htmlgen import a from htmlgen import a
from os import getEnv from os import getEnv
@@ -15,6 +15,33 @@ import routes/[
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
const issuesUrl = "https://github.com/zedeus/nitter/issues" const issuesUrl = "https://github.com/zedeus/nitter/issues"
type ColoredLogger = ref object of Logger
method log(logger: ColoredLogger, level: Level, args: varargs[string, `$`]) =
if level < logger.levelThreshold: return
let color = case level
of lvlFatal, lvlError: fgRed
of lvlWarn: fgYellow
of lvlInfo: fgGreen
of lvlDebug: fgCyan
else: fgWhite
let levelStr = case level
of lvlFatal: "fatal"
of lvlError: "error"
of lvlWarn: "warn"
of lvlInfo: "info"
of lvlDebug: "debug"
else: "other"
let timeStr = format(now(), "HH:mm:ss")
stdout.styledWrite(fgWhite, "[", timeStr, "] ", color, levelStr, fgWhite, ": ")
for arg in args:
stdout.write(arg)
stdout.write("\n")
stdout.flushFile()
let let
configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf") configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf")
(cfg, fullCfg) = getConfig(configPath) (cfg, fullCfg) = getConfig(configPath)
@@ -23,13 +50,14 @@ let
initSessionPool(cfg, sessionsPath) initSessionPool(cfg, sessionsPath)
if not cfg.enableDebug: addHandler(new(ColoredLogger))
# Silence Jester's query warning
addHandler(newConsoleLogger())
setLogFilter(lvlError)
stdout.write &"Starting Nitter at {getUrlPrefix(cfg)}\n" if cfg.enableDebug:
stdout.flushFile setLogFilter(lvlDebug)
else:
setLogFilter(lvlInfo)
info &"Starting Nitter at {getUrlPrefix(cfg)}"
updateDefaultPrefs(fullCfg) updateDefaultPrefs(fullCfg)
setCacheTimes(cfg) setCacheTimes(cfg)
@@ -40,8 +68,7 @@ setHttpProxy(cfg.proxy, cfg.proxyAuth)
initAboutPage(cfg.staticDir) initAboutPage(cfg.staticDir)
waitFor initRedisPool(cfg) waitFor initRedisPool(cfg)
stdout.write &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}\n" info &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}"
stdout.flushFile
createUnsupportedRouter(cfg) createUnsupportedRouter(cfg)
createResolverRouter(cfg) createResolverRouter(cfg)
@@ -83,13 +110,13 @@ routes:
resp Http404, showError("Page not found", cfg) resp Http404, showError("Page not found", cfg)
error InternalError: error InternalError:
echo error.exc.name, ": ", error.exc.msg error error.exc.name, ": ", error.exc.msg
const link = a("open a GitHub issue", href = issuesUrl) const link = a("open a GitHub issue", href = issuesUrl)
resp Http500, showError( resp Http500, showError(
&"An error occurred, please {link} with the URL you tried to visit.", cfg) &"An error occurred, please {link} with the URL you tried to visit.", cfg)
error BadClientError: error BadClientError:
echo error.exc.name, ": ", error.exc.msg error error.exc.name, ": ", error.exc.msg
resp Http500, showError("Network error occurred, please try again.", cfg) resp Http500, showError("Network error occurred, please try again.", cfg)
error RateLimitError: error RateLimitError:

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import std/[times, macros, htmlgen, options, algorithm, re] import std/[times, macros, htmlgen, options, algorithm, re, logging]
import std/strutils except escape import std/strutils except escape
import std/unicode except strip import std/unicode except strip
from xmltree import escape from xmltree import escape
@@ -84,7 +84,7 @@ proc getEntryId*(js: JsonNode): string {.inline.} =
elif "tombstone" in entry: elif "tombstone" in entry:
return js{"content", "item", "content", "tombstone", "tweet", "id"}.getStr return js{"content", "item", "content", "tombstone", "tweet", "id"}.getStr
else: else:
echo "unknown entry: ", entry warn "unknown entry: ", entry
return return
template getStrVal*(js: JsonNode; default=""): string = template getStrVal*(js: JsonNode; default=""): string =

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, times, strformat, strutils, tables, hashes import asyncdispatch, times, strformat, strutils, tables, hashes, logging
import redis, redpool, flatty, supersnappy import redis, redpool, flatty, supersnappy
import types, api import types, api
@@ -59,8 +59,7 @@ proc initRedisPool*(cfg: Config) {.async.} =
await r.configSet("hash-max-ziplist-entries", "1000") await r.configSet("hash-max-ziplist-entries", "1000")
except OSError: except OSError:
stdout.write "Failed to connect to Redis.\n" fatal "Failed to connect to Redis."
stdout.flushFile
quit(1) quit(1)
template uidKey(name: string): string = "pid:" & $(hash(name) div 1_000_000) template uidKey(name: string): string = "pid:" & $(hash(name) div 1_000_000)
@@ -112,7 +111,7 @@ template deserialize(data, T) =
try: try:
result = fromFlatty(uncompress(data), T) result = fromFlatty(uncompress(data), T)
except: except:
echo "Decompression failed($#): '$#'" % [astToStr(T), data] error "Decompression failed($#): '$#'" % [astToStr(T), data]
proc getUserId*(username: string): Future[string] {.async.} = proc getUserId*(username: string): Future[string] {.async.} =
let name = toLower(username) let name = toLower(username)
@@ -189,6 +188,6 @@ proc getCachedRss*(key: string): Future[Rss] {.async.} =
let feed = await r.hGet(k, "rss") let feed = await r.hGet(k, "rss")
if feed.len > 0 and feed != redisNil: if feed.len > 0 and feed != redisNil:
try: result.feed = uncompress feed try: result.feed = uncompress feed
except: echo "Decompressing RSS failed: ", feed except: error "Decompressing RSS failed: ", feed
else: else:
result.cursor.setLen 0 result.cursor.setLen 0

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import uri, strutils, httpclient, os, hashes, base64, re import uri, strutils, httpclient, os, hashes, base64, re, logging
import asynchttpserver, asyncstreams, asyncfile, asyncnet import asynchttpserver, asyncstreams, asyncfile, asyncnet
import jester import jester
@@ -38,7 +38,7 @@ proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
let res = await client.get(url) let res = await client.get(url)
if res.status != "200 OK": if res.status != "200 OK":
if res.status != "404 Not Found": if res.status != "404 Not Found":
echo "[media] Proxying failed, status: $1, url: $2" % [res.status, url] warn "[media] Proxying failed, status: $1, url: $2" % [res.status, url]
return Http404 return Http404
let hashed = $hash(url) let hashed = $hash(url)
@@ -67,7 +67,7 @@ proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
await request.client.send(data) await request.client.send(data)
data.setLen 0 data.setLen 0
except HttpRequestError, ProtocolError, OSError: except HttpRequestError, ProtocolError, OSError:
echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url] error "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url]
result = Http404 result = Http404
finally: finally:
client.close() client.close()

View File

@@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, strutils, sequtils, uri, options, sugar import asyncdispatch, strutils, sequtils, uri, options, sugar, logging
import jester, karax/vdom import jester, karax/vdom
@@ -32,7 +32,7 @@ proc createStatusRouter*(cfg: Config) =
let conv = await getTweet(id, getCursor()) let conv = await getTweet(id, getCursor())
if conv == nil: if conv == nil:
echo "nil conv" warn "nil conv"
if conv == nil or conv.tweet == nil or conv.tweet.id == 0: if conv == nil or conv.tweet == nil or conv.tweet.id == 0:
var error = "Tweet not found" var error = "Tweet not found"