mirror of
https://github.com/zedeus/nitter.git
synced 2025-12-05 19:45:36 -05:00
Compare commits
7 Commits
b83227aaf5
...
71e65c84d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71e65c84d7 | ||
|
|
436a873e4b | ||
|
|
96ec75fc7f | ||
|
|
7a08a9e132 | ||
|
|
31d210ca47 | ||
|
|
dae68b4f13 | ||
|
|
8516ebe2b7 |
@@ -26,6 +26,7 @@ enableRSS = true # set this to false to disable RSS feeds
|
|||||||
enableDebug = false # enable request logs and debug endpoints (/.sessions)
|
enableDebug = false # enable request logs and debug endpoints (/.sessions)
|
||||||
proxy = "" # http/https url, SOCKS proxies are not supported
|
proxy = "" # http/https url, SOCKS proxies are not supported
|
||||||
proxyAuth = ""
|
proxyAuth = ""
|
||||||
|
disableTid = false # enable this if cookie-based auth is failing
|
||||||
|
|
||||||
# Change default preferences here, see src/prefs_impl.nim for a complete list
|
# Change default preferences here, see src/prefs_impl.nim for a complete list
|
||||||
[Preferences]
|
[Preferences]
|
||||||
|
|||||||
145
public/css/fontello.css
vendored
145
public/css/fontello.css
vendored
@@ -1,53 +1,138 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'fontello';
|
font-family: "fontello";
|
||||||
src: url('/fonts/fontello.eot?61663884');
|
src: url("/fonts/fontello.eot?77185648");
|
||||||
src: url('/fonts/fontello.eot?61663884#iefix') format('embedded-opentype'),
|
src:
|
||||||
url('/fonts/fontello.woff2?61663884') format('woff2'),
|
url("/fonts/fontello.eot?77185648#iefix") format("embedded-opentype"),
|
||||||
url('/fonts/fontello.woff?61663884') format('woff'),
|
url("/fonts/fontello.woff2?77185648") format("woff2"),
|
||||||
url('/fonts/fontello.ttf?61663884') format('truetype'),
|
url("/fonts/fontello.woff?77185648") format("woff"),
|
||||||
url('/fonts/fontello.svg?61663884#fontello') format('svg');
|
url("/fonts/fontello.ttf?77185648") format("truetype"),
|
||||||
|
url("/fonts/fontello.svg?77185648#fontello") format("svg");
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
|
||||||
|
[class^="icon-"]:before,
|
||||||
|
[class*=" icon-"]:before {
|
||||||
font-family: "fontello";
|
font-family: "fontello";
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
speak: never;
|
speak: never;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
|
margin-right: 0.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
/* For safety - reset parent styles, that can break glyph codes*/
|
/* For safety - reset parent styles, that can break glyph codes*/
|
||||||
font-variant: normal;
|
font-variant: normal;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|
||||||
/* fix buttons height, for twitter bootstrap */
|
/* fix buttons height, for twitter bootstrap */
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
|
|
||||||
/* Font smoothing. That was taken from TWBS */
|
/* Font smoothing. That was taken from TWBS */
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-views:before { content: '\e800'; } /* '' */
|
.icon-views:before {
|
||||||
.icon-heart:before { content: '\e801'; } /* '' */
|
content: "\e800";
|
||||||
.icon-quote:before { content: '\e802'; } /* '' */
|
}
|
||||||
.icon-comment:before { content: '\e803'; } /* '' */
|
|
||||||
.icon-ok:before { content: '\e804'; } /* '' */
|
/* '' */
|
||||||
.icon-play:before { content: '\e805'; } /* '' */
|
.icon-heart:before {
|
||||||
.icon-link:before { content: '\e806'; } /* '' */
|
content: "\e801";
|
||||||
.icon-calendar:before { content: '\e807'; } /* '' */
|
}
|
||||||
.icon-location:before { content: '\e808'; } /* '' */
|
|
||||||
.icon-picture:before { content: '\e809'; } /* '' */
|
/* '' */
|
||||||
.icon-lock:before { content: '\e80a'; } /* '' */
|
.icon-quote:before {
|
||||||
.icon-down:before { content: '\e80b'; } /* '' */
|
content: "\e802";
|
||||||
.icon-retweet:before { content: '\e80c'; } /* '' */
|
}
|
||||||
.icon-search:before { content: '\e80d'; } /* '' */
|
|
||||||
.icon-pin:before { content: '\e80e'; } /* '' */
|
/* '' */
|
||||||
.icon-cog:before { content: '\e80f'; } /* '' */
|
.icon-comment:before {
|
||||||
.icon-rss:before { content: '\e810'; } /* '' */
|
content: "\e803";
|
||||||
.icon-info:before { content: '\f128'; } /* '' */
|
}
|
||||||
.icon-bird:before { content: '\f309'; } /* '' */
|
|
||||||
|
/* '' */
|
||||||
|
.icon-play:before {
|
||||||
|
content: "\e805";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-link:before {
|
||||||
|
content: "\e806";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-calendar:before {
|
||||||
|
content: "\e807";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-location:before {
|
||||||
|
content: "\e808";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-picture:before {
|
||||||
|
content: "\e809";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-lock:before {
|
||||||
|
content: "\e80a";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-down:before {
|
||||||
|
content: "\e80b";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-retweet:before {
|
||||||
|
content: "\e80c";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-search:before {
|
||||||
|
content: "\e80d";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-pin:before {
|
||||||
|
content: "\e80e";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-cog:before {
|
||||||
|
content: "\e80f";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-rss:before {
|
||||||
|
content: "\e810";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-ok:before {
|
||||||
|
content: "\e811";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-circle:before {
|
||||||
|
content: "\f111";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-info:before {
|
||||||
|
content: "\f128";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
.icon-bird:before {
|
||||||
|
content: "\f309";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* '' */
|
||||||
|
|||||||
Binary file not shown.
@@ -14,8 +14,6 @@
|
|||||||
|
|
||||||
<glyph glyph-name="comment" unicode="" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
|
<glyph glyph-name="comment" unicode="" d="M1000 350q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12-10-1-17 5t-10 16v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 73 40 139t106 114 160 76 194 28q136 0 251-48t182-130 67-179z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
<glyph glyph-name="ok" unicode="" d="M0 260l162 162 166-164 508 510 164-164-510-510-162-162-162 164z" horiz-adv-x="1000" />
|
|
||||||
|
|
||||||
<glyph glyph-name="play" unicode="" d="M772 333l-741-412q-13-7-22-2t-9 20v822q0 14 9 20t22-2l741-412q13-7 13-17t-13-17z" horiz-adv-x="785.7" />
|
<glyph glyph-name="play" unicode="" d="M772 333l-741-412q-13-7-22-2t-9 20v822q0 14 9 20t22-2l741-412q13-7 13-17t-13-17z" horiz-adv-x="785.7" />
|
||||||
|
|
||||||
<glyph glyph-name="link" unicode="" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" />
|
<glyph glyph-name="link" unicode="" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" />
|
||||||
@@ -40,6 +38,10 @@
|
|||||||
|
|
||||||
<glyph glyph-name="rss" unicode="" d="M184 93c0-51-43-91-93-91s-91 40-91 91c0 50 41 91 91 91s93-41 93-91z m261-85l-125 0c0 174-140 323-315 323l0 118c231 0 440-163 440-441z m259 0l-136 0c0 300-262 561-563 561l0 129c370 0 699-281 699-690z" horiz-adv-x="704" />
|
<glyph glyph-name="rss" unicode="" d="M184 93c0-51-43-91-93-91s-91 40-91 91c0 50 41 91 91 91s93-41 93-91z m261-85l-125 0c0 174-140 323-315 323l0 118c231 0 440-163 440-441z m259 0l-136 0c0 300-262 561-563 561l0 129c370 0 699-281 699-690z" horiz-adv-x="704" />
|
||||||
|
|
||||||
|
<glyph glyph-name="ok" unicode="" d="M933 534q0-22-16-38l-404-404-76-76q-16-15-38-15t-38 15l-76 76-202 202q-15 16-15 38t15 38l76 76q16 16 38 16t38-16l164-165 366 367q16 16 38 16t38-16l76-76q16-15 16-38z" horiz-adv-x="1000" />
|
||||||
|
|
||||||
|
<glyph glyph-name="circle" unicode="" d="M857 350q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
|
||||||
|
|
||||||
<glyph glyph-name="info" unicode="" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
|
<glyph glyph-name="info" unicode="" d="M393 149v-134q0-9-7-15t-15-7h-134q-9 0-16 7t-7 15v134q0 9 7 16t16 6h134q9 0 15-6t7-16z m176 335q0-30-8-56t-20-43-31-33-32-25-34-19q-23-13-38-37t-15-37q0-10-7-18t-16-9h-134q-8 0-14 11t-6 20v26q0 46 37 87t79 60q33 16 47 32t14 42q0 24-26 41t-60 18q-36 0-60-16-20-14-60-64-7-9-17-9-7 0-14 4l-91 70q-8 6-9 14t3 16q89 148 259 148 45 0 90-17t81-46 59-72 23-88z" horiz-adv-x="571.4" />
|
||||||
|
|
||||||
<glyph glyph-name="bird" unicode="" d="M920 636q-36-54-94-98l0-24q0-130-60-250t-186-203-290-83q-160 0-290 84 14-2 46-2 132 0 234 80-62 2-110 38t-66 94q10-4 34-4 26 0 50 6-66 14-108 66t-42 120l0 2q36-20 84-24-84 58-84 158 0 48 26 94 154-188 390-196-6 18-6 42 0 78 55 133t135 55q82 0 136-58 60 12 120 44-20-66-82-104 56 8 108 30z" horiz-adv-x="920" />
|
<glyph glyph-name="bird" unicode="" d="M920 636q-36-54-94-98l0-24q0-130-60-250t-186-203-290-83q-160 0-290 84 14-2 46-2 132 0 234 80-62 2-110 38t-66 94q10-4 34-4 26 0 50 6-66 14-108 66t-42 120l0 2q36-20 84-24-84 58-84 158 0 48 26 94 154-188 390-196-6 18-6 42 0 78 55 133t135 55q82 0 136-58 60 12 120 44-20-66-82-104 56 8 108 30z" horiz-adv-x="920" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
131
src/api.nim
131
src/api.nim
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import asyncdispatch, httpclient, uri, strutils, sequtils, sugar, tables
|
import asyncdispatch, httpclient, strutils, sequtils, sugar
|
||||||
import packedjson
|
import packedjson
|
||||||
import types, query, formatters, consts, apiutils, parser
|
import types, query, formatters, consts, apiutils, parser
|
||||||
import experimental/parser as newParser
|
import experimental/parser as newParser
|
||||||
@@ -11,95 +11,91 @@ proc genParams(variables: string; fieldToggles = ""): seq[(string, string)] =
|
|||||||
if fieldToggles.len > 0:
|
if fieldToggles.len > 0:
|
||||||
result.add ("fieldToggles", fieldToggles)
|
result.add ("fieldToggles", fieldToggles)
|
||||||
|
|
||||||
proc mediaUrl(id: string; cursor: string): SessionAwareUrl =
|
proc apiUrl(endpoint, variables: string; fieldToggles = ""): ApiUrl =
|
||||||
let
|
return ApiUrl(endpoint: endpoint, params: genParams(variables, fieldToggles))
|
||||||
cookieVars = userMediaVars % [id, cursor]
|
|
||||||
oauthVars = restIdVars % [id, cursor]
|
proc apiReq(endpoint, variables: string; fieldToggles = ""): ApiReq =
|
||||||
result = SessionAwareUrl(
|
let url = apiUrl(endpoint, variables, fieldToggles)
|
||||||
cookieUrl: graphUserMedia ? genParams(cookieVars),
|
return ApiReq(cookie: url, oauth: url)
|
||||||
oauthUrl: graphUserMediaV2 ? genParams(oauthVars)
|
|
||||||
|
proc mediaUrl(id: string; cursor: string): ApiReq =
|
||||||
|
result = ApiReq(
|
||||||
|
cookie: apiUrl(graphUserMedia, userMediaVars % [id, cursor]),
|
||||||
|
oauth: apiUrl(graphUserMediaV2, restIdVars % [id, cursor])
|
||||||
)
|
)
|
||||||
|
|
||||||
proc userTweetsUrl(id: string; cursor: string): SessionAwareUrl =
|
proc userTweetsUrl(id: string; cursor: string): ApiReq =
|
||||||
let
|
result = ApiReq(
|
||||||
cookieVars = userTweetsVars % [id, cursor]
|
# cookie: apiUrl(graphUserTweets, userTweetsVars % [id, cursor], userTweetsFieldToggles),
|
||||||
oauthVars = restIdVars % [id, cursor]
|
oauth: apiUrl(graphUserTweetsV2, restIdVars % [id, cursor])
|
||||||
result = SessionAwareUrl(
|
|
||||||
# cookieUrl: graphUserTweets ? genParams(cookieVars, userTweetsFieldToggles),
|
|
||||||
oauthUrl: graphUserTweetsV2 ? genParams(oauthVars)
|
|
||||||
)
|
)
|
||||||
# might change this in the future pending testing
|
# might change this in the future pending testing
|
||||||
result.cookieUrl = result.oauthUrl
|
result.cookie = result.oauth
|
||||||
|
|
||||||
proc userTweetsAndRepliesUrl(id: string; cursor: string): SessionAwareUrl =
|
proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq =
|
||||||
let
|
let cookieVars = userTweetsAndRepliesVars % [id, cursor]
|
||||||
cookieVars = userTweetsAndRepliesVars % [id, cursor]
|
result = ApiReq(
|
||||||
oauthVars = restIdVars % [id, cursor]
|
cookie: apiUrl(graphUserTweetsAndReplies, cookieVars, userTweetsFieldToggles),
|
||||||
result = SessionAwareUrl(
|
oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor])
|
||||||
cookieUrl: graphUserTweetsAndReplies ? genParams(cookieVars, userTweetsFieldToggles),
|
|
||||||
oauthUrl: graphUserTweetsAndRepliesV2 ? genParams(oauthVars)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
proc tweetDetailUrl(id: string; cursor: string): SessionAwareUrl =
|
proc tweetDetailUrl(id: string; cursor: string): ApiReq =
|
||||||
let
|
let cookieVars = tweetDetailVars % [id, cursor]
|
||||||
cookieVars = tweetDetailVars % [id, cursor]
|
result = ApiReq(
|
||||||
oauthVars = tweetVars % [id, cursor]
|
cookie: apiUrl(graphTweetDetail, cookieVars, tweetDetailFieldToggles),
|
||||||
result = SessionAwareUrl(
|
oauth: apiUrl(graphTweet, tweetVars % [id, cursor])
|
||||||
cookieUrl: graphTweetDetail ? genParams(cookieVars, tweetDetailFieldToggles),
|
|
||||||
oauthUrl: graphTweet ? genParams(oauthVars)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
proc userUrl(username: string): SessionAwareUrl =
|
proc userUrl(username: string): ApiReq =
|
||||||
let
|
let cookieVars = """{"screen_name":"$1","withGrokTranslatedBio":false}""" % username
|
||||||
cookieVars = """{"screen_name":"$1","withGrokTranslatedBio":false}""" % username
|
result = ApiReq(
|
||||||
oauthVars = """{"screen_name": "$1"}""" % username
|
cookie: apiUrl(graphUser, cookieVars, tweetDetailFieldToggles),
|
||||||
result = SessionAwareUrl(
|
oauth: apiUrl(graphUserV2, """{"screen_name": "$1"}""" % username)
|
||||||
cookieUrl: graphUser ? genParams(cookieVars, tweetDetailFieldToggles),
|
|
||||||
oauthUrl: graphUserV2 ? genParams(oauthVars)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
proc getGraphUser*(username: string): Future[User] {.async.} =
|
proc getGraphUser*(username: string): Future[User] {.async.} =
|
||||||
if username.len == 0: return
|
if username.len == 0: return
|
||||||
let js = await fetchRaw(userUrl(username), Api.userScreenName)
|
let js = await fetchRaw(userUrl(username))
|
||||||
result = parseGraphUser(js)
|
result = parseGraphUser(js)
|
||||||
|
|
||||||
proc getGraphUserById*(id: string): Future[User] {.async.} =
|
proc getGraphUserById*(id: string): Future[User] {.async.} =
|
||||||
if id.len == 0 or id.any(c => not c.isDigit): return
|
if id.len == 0 or id.any(c => not c.isDigit): return
|
||||||
let
|
let
|
||||||
url = graphUserById ? genParams("""{"rest_id": "$1"}""" % id)
|
url = apiReq(graphUserById, """{"rest_id": "$1"}""" % id)
|
||||||
js = await fetchRaw(url, Api.userRestId)
|
js = await fetchRaw(url)
|
||||||
result = parseGraphUser(js)
|
result = parseGraphUser(js)
|
||||||
|
|
||||||
proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profile] {.async.} =
|
proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profile] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
let
|
let
|
||||||
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
js = case kind
|
url = case kind
|
||||||
of TimelineKind.tweets:
|
of TimelineKind.tweets: userTweetsUrl(id, cursor)
|
||||||
await fetch(userTweetsUrl(id, cursor), Api.userTweets)
|
of TimelineKind.replies: userTweetsAndRepliesUrl(id, cursor)
|
||||||
of TimelineKind.replies:
|
of TimelineKind.media: mediaUrl(id, cursor)
|
||||||
await fetch(userTweetsAndRepliesUrl(id, cursor), Api.userTweetsAndReplies)
|
js = await fetch(url)
|
||||||
of TimelineKind.media:
|
|
||||||
await fetch(mediaUrl(id, cursor), Api.userMedia)
|
|
||||||
result = parseGraphTimeline(js, after)
|
result = parseGraphTimeline(js, after)
|
||||||
|
|
||||||
proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
|
proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
let
|
let
|
||||||
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
url = graphListTweets ? genParams(restIdVars % [id, cursor])
|
url = apiReq(graphListTweets, restIdVars % [id, cursor])
|
||||||
result = parseGraphTimeline(await fetch(url, Api.listTweets), after).tweets
|
js = await fetch(url)
|
||||||
|
result = parseGraphTimeline(js, after).tweets
|
||||||
|
|
||||||
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
|
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
|
||||||
let
|
let
|
||||||
variables = %*{"screenName": name, "listSlug": list}
|
variables = %*{"screenName": name, "listSlug": list}
|
||||||
url = graphListBySlug ? genParams($variables)
|
url = apiReq(graphListBySlug, $variables)
|
||||||
result = parseGraphList(await fetch(url, Api.listBySlug))
|
js = await fetch(url)
|
||||||
|
result = parseGraphList(js)
|
||||||
|
|
||||||
proc getGraphList*(id: string): Future[List] {.async.} =
|
proc getGraphList*(id: string): Future[List] {.async.} =
|
||||||
let
|
let
|
||||||
url = graphListById ? genParams("""{"listId": "$1"}""" % id)
|
url = apiReq(graphListById, """{"listId": "$1"}""" % id)
|
||||||
result = parseGraphList(await fetch(url, Api.list))
|
js = await fetch(url)
|
||||||
|
result = parseGraphList(js)
|
||||||
|
|
||||||
proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.} =
|
proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.} =
|
||||||
if list.id.len == 0: return
|
if list.id.len == 0: return
|
||||||
@@ -113,22 +109,23 @@ proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.}
|
|||||||
}
|
}
|
||||||
if after.len > 0:
|
if after.len > 0:
|
||||||
variables["cursor"] = % after
|
variables["cursor"] = % after
|
||||||
let url = graphListMembers ? genParams($variables)
|
let
|
||||||
result = parseGraphListMembers(await fetchRaw(url, Api.listMembers), after)
|
url = apiReq(graphListMembers, $variables)
|
||||||
|
js = await fetchRaw(url)
|
||||||
|
result = parseGraphListMembers(js, after)
|
||||||
|
|
||||||
proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
|
proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
let
|
let
|
||||||
variables = """{"rest_id": "$1"}""" % id
|
url = apiReq(graphTweetResult, """{"rest_id": "$1"}""" % id)
|
||||||
params = {"variables": variables, "features": gqlFeatures}
|
js = await fetch(url)
|
||||||
js = await fetch(graphTweetResult ? params, Api.tweetResult)
|
|
||||||
result = parseGraphTweetResult(js)
|
result = parseGraphTweetResult(js)
|
||||||
|
|
||||||
proc getGraphTweet(id: string; after=""): Future[Conversation] {.async.} =
|
proc getGraphTweet(id: string; after=""): Future[Conversation] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
let
|
let
|
||||||
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
js = await fetch(tweetDetailUrl(id, cursor), Api.tweetDetail)
|
js = await fetch(tweetDetailUrl(id, cursor))
|
||||||
result = parseGraphConversation(js, id)
|
result = parseGraphConversation(js, id)
|
||||||
|
|
||||||
proc getReplies*(id, after: string): Future[Result[Chain]] {.async.} =
|
proc getReplies*(id, after: string): Future[Result[Chain]] {.async.} =
|
||||||
@@ -157,8 +154,10 @@ proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
|
|||||||
}
|
}
|
||||||
if after.len > 0:
|
if after.len > 0:
|
||||||
variables["cursor"] = % after
|
variables["cursor"] = % after
|
||||||
let url = graphSearchTimeline ? genParams($variables)
|
let
|
||||||
result = parseGraphSearch[Tweets](await fetch(url, Api.search), after)
|
url = apiReq(graphSearchTimeline, $variables)
|
||||||
|
js = await fetch(url)
|
||||||
|
result = parseGraphSearch[Tweets](js, after)
|
||||||
result.query = query
|
result.query = query
|
||||||
|
|
||||||
proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.} =
|
proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.} =
|
||||||
@@ -179,13 +178,15 @@ proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] {.async.}
|
|||||||
variables["cursor"] = % after
|
variables["cursor"] = % after
|
||||||
result.beginning = false
|
result.beginning = false
|
||||||
|
|
||||||
let url = graphSearchTimeline ? genParams($variables)
|
let
|
||||||
result = parseGraphSearch[User](await fetch(url, Api.search), after)
|
url = apiReq(graphSearchTimeline, $variables)
|
||||||
|
js = await fetch(url)
|
||||||
|
result = parseGraphSearch[User](js, after)
|
||||||
result.query = query
|
result.query = query
|
||||||
|
|
||||||
proc getPhotoRail*(id: string): Future[PhotoRail] {.async.} =
|
proc getPhotoRail*(id: string): Future[PhotoRail] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
let js = await fetch(mediaUrl(id, ""), Api.userMedia)
|
let js = await fetch(mediaUrl(id, ""))
|
||||||
result = parseGraphPhotoRail(js)
|
result = parseGraphPhotoRail(js)
|
||||||
|
|
||||||
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
|
proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} =
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
# 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
|
||||||
import jsony, packedjson, zippy, oauth1
|
import jsony, packedjson, zippy, oauth1
|
||||||
import types, auth, consts, parserutils, http_pool
|
import types, auth, consts, parserutils, http_pool, tid
|
||||||
import experimental/types/common
|
import experimental/types/common
|
||||||
|
|
||||||
const
|
const
|
||||||
rlRemaining = "x-rate-limit-remaining"
|
rlRemaining = "x-rate-limit-remaining"
|
||||||
rlReset = "x-rate-limit-reset"
|
rlReset = "x-rate-limit-reset"
|
||||||
rlLimit = "x-rate-limit-limit"
|
rlLimit = "x-rate-limit-limit"
|
||||||
errorsToSkip = {doesntExist, tweetNotFound, timeout, unauthorized, badRequest}
|
errorsToSkip = {null, doesntExist, tweetNotFound, timeout, unauthorized, badRequest}
|
||||||
|
|
||||||
var pool: HttpPool
|
var
|
||||||
|
pool: HttpPool
|
||||||
|
disableTid: bool
|
||||||
|
|
||||||
|
proc setDisableTid*(disable: bool) =
|
||||||
|
disableTid = disable
|
||||||
|
|
||||||
|
proc toUrl(req: ApiReq; sessionKind: SessionKind): Uri =
|
||||||
|
case sessionKind
|
||||||
|
of oauth:
|
||||||
|
let o = req.oauth
|
||||||
|
parseUri("https://api.x.com/graphql") / o.endpoint ? o.params
|
||||||
|
of cookie:
|
||||||
|
let c = req.cookie
|
||||||
|
parseUri("https://x.com/i/api/graphql") / c.endpoint ? c.params
|
||||||
|
|
||||||
proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
|
proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
|
||||||
let
|
let
|
||||||
@@ -32,15 +46,15 @@ proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string =
|
|||||||
proc getCookieHeader(authToken, ct0: string): string =
|
proc getCookieHeader(authToken, ct0: string): string =
|
||||||
"auth_token=" & authToken & "; ct0=" & ct0
|
"auth_token=" & authToken & "; ct0=" & ct0
|
||||||
|
|
||||||
proc genHeaders*(session: Session, url: string): HttpHeaders =
|
proc genHeaders*(session: Session, url: Uri): Future[HttpHeaders] {.async.} =
|
||||||
result = newHttpHeaders({
|
result = newHttpHeaders({
|
||||||
"connection": "keep-alive",
|
"connection": "keep-alive",
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-twitter-active-user": "yes",
|
"x-twitter-active-user": "yes",
|
||||||
"x-twitter-client-language": "en",
|
"x-twitter-client-language": "en",
|
||||||
"authority": "api.x.com",
|
"origin": "https://x.com",
|
||||||
"accept-encoding": "gzip",
|
"accept-encoding": "gzip",
|
||||||
"accept-language": "en-US,en;q=0.9",
|
"accept-language": "en-US,en;q=0.5",
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||||
@@ -48,15 +62,20 @@ proc genHeaders*(session: Session, url: string): HttpHeaders =
|
|||||||
|
|
||||||
case session.kind
|
case session.kind
|
||||||
of SessionKind.oauth:
|
of SessionKind.oauth:
|
||||||
result["authorization"] = getOauthHeader(url, session.oauthToken, session.oauthSecret)
|
result["authority"] = "api.x.com"
|
||||||
|
result["authorization"] = getOauthHeader($url, session.oauthToken, session.oauthSecret)
|
||||||
of SessionKind.cookie:
|
of SessionKind.cookie:
|
||||||
result["authorization"] = "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"
|
|
||||||
result["x-twitter-auth-type"] = "OAuth2Session"
|
result["x-twitter-auth-type"] = "OAuth2Session"
|
||||||
result["x-csrf-token"] = session.ct0
|
result["x-csrf-token"] = session.ct0
|
||||||
result["cookie"] = getCookieHeader(session.authToken, session.ct0)
|
result["cookie"] = getCookieHeader(session.authToken, session.ct0)
|
||||||
|
if disableTid:
|
||||||
|
result["authorization"] = bearerToken2
|
||||||
|
else:
|
||||||
|
result["authorization"] = bearerToken
|
||||||
|
result["x-client-transaction-id"] = await genTid(url.path)
|
||||||
|
|
||||||
proc getAndValidateSession*(api: Api): Future[Session] {.async.} =
|
proc getAndValidateSession*(req: ApiReq): Future[Session] {.async.} =
|
||||||
result = await getSession(api)
|
result = await getSession(req)
|
||||||
case result.kind
|
case result.kind
|
||||||
of SessionKind.oauth:
|
of SessionKind.oauth:
|
||||||
if result.oauthToken.len == 0:
|
if result.oauthToken.len == 0:
|
||||||
@@ -73,7 +92,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
var resp: AsyncResponse
|
var resp: AsyncResponse
|
||||||
pool.use(genHeaders(session, $url)):
|
pool.use(await genHeaders(session, url)):
|
||||||
template getContent =
|
template getContent =
|
||||||
resp = await c.get($url)
|
resp = await c.get($url)
|
||||||
result = await resp.body
|
result = await resp.body
|
||||||
@@ -89,7 +108,7 @@ template fetchImpl(result, fetchBody) {.dirty.} =
|
|||||||
remaining = parseInt(resp.headers[rlRemaining])
|
remaining = parseInt(resp.headers[rlRemaining])
|
||||||
reset = parseInt(resp.headers[rlReset])
|
reset = parseInt(resp.headers[rlReset])
|
||||||
limit = parseInt(resp.headers[rlLimit])
|
limit = parseInt(resp.headers[rlLimit])
|
||||||
session.setRateLimit(api, remaining, reset, limit)
|
session.setRateLimit(req, remaining, reset, limit)
|
||||||
|
|
||||||
if result.len > 0:
|
if result.len > 0:
|
||||||
if resp.headers.getOrDefault("content-encoding") == "gzip":
|
if resp.headers.getOrDefault("content-encoding") == "gzip":
|
||||||
@@ -98,24 +117,22 @@ 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
|
echo "Fetch error, API: ", url.path, ", errors: ", errors
|
||||||
if errors in {expiredToken, badToken, locked}:
|
if errors in {expiredToken, badToken, locked}:
|
||||||
invalidate(session)
|
invalidate(session)
|
||||||
raise rateLimitError()
|
raise rateLimitError()
|
||||||
elif errors in {rateLimited}:
|
elif errors in {rateLimited}:
|
||||||
# rate limit hit, resets after 24 hours
|
# rate limit hit, resets after 24 hours
|
||||||
setLimited(session, api)
|
setLimited(session, req)
|
||||||
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
|
echo "[sessions] 429 error, API: ", url.path, ", session: ", session.pretty
|
||||||
session.apis[api].remaining = 0
|
|
||||||
# rate limit hit, resets after the 15 minute window
|
|
||||||
raise rateLimitError()
|
raise rateLimitError()
|
||||||
|
|
||||||
fetchBody
|
fetchBody
|
||||||
|
|
||||||
if resp.status == $Http400:
|
if resp.status == $Http400:
|
||||||
echo "ERROR 400, ", api, ": ", result
|
echo "ERROR 400, ", url.path, ": ", result
|
||||||
raise newException(InternalError, $url)
|
raise newException(InternalError, $url)
|
||||||
except InternalError as e:
|
except InternalError as e:
|
||||||
raise e
|
raise e
|
||||||
@@ -134,19 +151,16 @@ template retry(bod) =
|
|||||||
try:
|
try:
|
||||||
bod
|
bod
|
||||||
except RateLimitError:
|
except RateLimitError:
|
||||||
echo "[sessions] Rate limited, retrying ", api, " request..."
|
echo "[sessions] Rate limited, retrying ", req.cookie.endpoint, " request..."
|
||||||
bod
|
bod
|
||||||
|
|
||||||
proc fetch*(url: Uri | SessionAwareUrl; api: Api): Future[JsonNode] {.async.} =
|
proc fetch*(req: ApiReq): Future[JsonNode] {.async.} =
|
||||||
retry:
|
retry:
|
||||||
var
|
var
|
||||||
body: string
|
body: string
|
||||||
session = await getAndValidateSession(api)
|
session = await getAndValidateSession(req)
|
||||||
|
|
||||||
when url is SessionAwareUrl:
|
let url = req.toUrl(session.kind)
|
||||||
let url = case session.kind
|
|
||||||
of SessionKind.oauth: url.oauthUrl
|
|
||||||
of SessionKind.cookie: url.cookieUrl
|
|
||||||
|
|
||||||
fetchImpl body:
|
fetchImpl body:
|
||||||
if body.startsWith('{') or body.startsWith('['):
|
if body.startsWith('{') or body.startsWith('['):
|
||||||
@@ -157,19 +171,15 @@ proc fetch*(url: Uri | SessionAwareUrl; api: Api): Future[JsonNode] {.async.} =
|
|||||||
|
|
||||||
let error = result.getError
|
let error = result.getError
|
||||||
if error != null and error notin errorsToSkip:
|
if error != null and error notin errorsToSkip:
|
||||||
echo "Fetch error, API: ", api, ", error: ", error
|
echo "Fetch error, API: ", url.path, ", error: ", error
|
||||||
if error in {expiredToken, badToken, locked}:
|
if error in {expiredToken, badToken, locked}:
|
||||||
invalidate(session)
|
invalidate(session)
|
||||||
raise rateLimitError()
|
raise rateLimitError()
|
||||||
|
|
||||||
proc fetchRaw*(url: Uri | SessionAwareUrl; api: Api): Future[string] {.async.} =
|
proc fetchRaw*(req: ApiReq): Future[string] {.async.} =
|
||||||
retry:
|
retry:
|
||||||
var session = await getAndValidateSession(api)
|
var session = await getAndValidateSession(req)
|
||||||
|
let url = req.toUrl(session.kind)
|
||||||
when url is SessionAwareUrl:
|
|
||||||
let url = case session.kind
|
|
||||||
of SessionKind.oauth: url.oauthUrl
|
|
||||||
of SessionKind.cookie: url.cookieUrl
|
|
||||||
|
|
||||||
fetchImpl result:
|
fetchImpl result:
|
||||||
if not (result.startsWith('{') or result.startsWith('[')):
|
if not (result.startsWith('{') or result.startsWith('[')):
|
||||||
|
|||||||
32
src/auth.nim
32
src/auth.nim
@@ -1,6 +1,6 @@
|
|||||||
#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]
|
||||||
import types
|
import types, consts
|
||||||
import experimental/parser/session
|
import experimental/parser/session
|
||||||
|
|
||||||
# max requests at a time per session to avoid race conditions
|
# max requests at a time per session to avoid race conditions
|
||||||
@@ -15,6 +15,11 @@ var
|
|||||||
template log(str: varargs[string, `$`]) =
|
template log(str: varargs[string, `$`]) =
|
||||||
echo "[sessions] ", str.join("")
|
echo "[sessions] ", str.join("")
|
||||||
|
|
||||||
|
proc endpoint(req: ApiReq; session: Session): string =
|
||||||
|
case session.kind
|
||||||
|
of oauth: req.oauth.endpoint
|
||||||
|
of cookie: req.cookie.endpoint
|
||||||
|
|
||||||
proc pretty*(session: Session): string =
|
proc pretty*(session: Session): string =
|
||||||
if session.isNil:
|
if session.isNil:
|
||||||
return "<null>"
|
return "<null>"
|
||||||
@@ -122,11 +127,12 @@ proc rateLimitError*(): ref RateLimitError =
|
|||||||
proc noSessionsError*(): ref NoSessionsError =
|
proc noSessionsError*(): ref NoSessionsError =
|
||||||
newException(NoSessionsError, "no sessions available")
|
newException(NoSessionsError, "no sessions available")
|
||||||
|
|
||||||
proc isLimited(session: Session; api: Api): bool =
|
proc isLimited(session: Session; req: ApiReq): bool =
|
||||||
if session.isNil:
|
if session.isNil:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
if session.limited and api != Api.userTweets:
|
let api = req.endpoint(session)
|
||||||
|
if session.limited and api != graphUserTweetsV2:
|
||||||
if (epochTime().int - session.limitedAt) > hourInSeconds:
|
if (epochTime().int - session.limitedAt) > hourInSeconds:
|
||||||
session.limited = false
|
session.limited = false
|
||||||
log "resetting limit: ", session.pretty
|
log "resetting limit: ", session.pretty
|
||||||
@@ -140,8 +146,8 @@ proc isLimited(session: Session; api: Api): bool =
|
|||||||
else:
|
else:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
proc isReady(session: Session; api: Api): bool =
|
proc isReady(session: Session; req: ApiReq): bool =
|
||||||
not (session.isNil or session.pending > maxConcurrentReqs or session.isLimited(api))
|
not (session.isNil or session.pending > maxConcurrentReqs or session.isLimited(req))
|
||||||
|
|
||||||
proc invalidate*(session: var Session) =
|
proc invalidate*(session: var Session) =
|
||||||
if session.isNil: return
|
if session.isNil: return
|
||||||
@@ -156,24 +162,26 @@ proc release*(session: Session) =
|
|||||||
if session.isNil: return
|
if session.isNil: return
|
||||||
dec session.pending
|
dec session.pending
|
||||||
|
|
||||||
proc getSession*(api: Api): Future[Session] {.async.} =
|
proc getSession*(req: ApiReq): Future[Session] {.async.} =
|
||||||
for i in 0 ..< sessionPool.len:
|
for i in 0 ..< sessionPool.len:
|
||||||
if result.isReady(api): break
|
if result.isReady(req): break
|
||||||
result = sessionPool.sample()
|
result = sessionPool.sample()
|
||||||
|
|
||||||
if not result.isNil and result.isReady(api):
|
if not result.isNil and result.isReady(req):
|
||||||
inc result.pending
|
inc result.pending
|
||||||
else:
|
else:
|
||||||
log "no sessions available for API: ", api
|
log "no sessions available for API: ", req.cookie.endpoint
|
||||||
raise noSessionsError()
|
raise noSessionsError()
|
||||||
|
|
||||||
proc setLimited*(session: Session; api: Api) =
|
proc setLimited*(session: Session; req: ApiReq) =
|
||||||
|
let api = req.endpoint(session)
|
||||||
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
|
log "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; req: ApiReq; remaining, reset, limit: int) =
|
||||||
# avoid undefined behavior in race conditions
|
# avoid undefined behavior in race conditions
|
||||||
|
let api = req.endpoint(session)
|
||||||
if api in session.apis:
|
if api in session.apis:
|
||||||
let rateLimit = session.apis[api]
|
let rateLimit = session.apis[api]
|
||||||
if rateLimit.reset >= reset and rateLimit.remaining < remaining:
|
if rateLimit.reset >= reset and rateLimit.remaining < remaining:
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ proc getConfig*(path: string): (Config, parseCfg.Config) =
|
|||||||
enableRss: cfg.get("Config", "enableRSS", true),
|
enableRss: cfg.get("Config", "enableRSS", true),
|
||||||
enableDebug: cfg.get("Config", "enableDebug", false),
|
enableDebug: cfg.get("Config", "enableDebug", false),
|
||||||
proxy: cfg.get("Config", "proxy", ""),
|
proxy: cfg.get("Config", "proxy", ""),
|
||||||
proxyAuth: cfg.get("Config", "proxyAuth", "")
|
proxyAuth: cfg.get("Config", "proxyAuth", ""),
|
||||||
|
disableTid: cfg.get("Config", "disableTid", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (conf, cfg)
|
return (conf, cfg)
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import uri, strutils
|
import strutils
|
||||||
|
|
||||||
const
|
const
|
||||||
consumerKey* = "3nVuSoBZnx6U4vzUxf5w"
|
consumerKey* = "3nVuSoBZnx6U4vzUxf5w"
|
||||||
consumerSecret* = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"
|
consumerSecret* = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"
|
||||||
|
bearerToken* = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
||||||
|
bearerToken2* = "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"
|
||||||
|
|
||||||
gql = parseUri("https://api.x.com") / "graphql"
|
graphUser* = "-oaLodhGbbnzJBACb1kk2Q/UserByScreenName"
|
||||||
|
graphUserV2* = "WEoGnYB0EG1yGwamDCF6zg/UserResultByScreenNameQuery"
|
||||||
graphUser* = gql / "-oaLodhGbbnzJBACb1kk2Q/UserByScreenName"
|
graphUserById* = "VN33vKXrPT7p35DgNR27aw/UserResultByIdQuery"
|
||||||
graphUserV2* = gql / "WEoGnYB0EG1yGwamDCF6zg/UserResultByScreenNameQuery"
|
graphUserTweetsV2* = "6QdSuZ5feXxOadEdXa4XZg/UserWithProfileTweetsQueryV2"
|
||||||
graphUserById* = gql / "VN33vKXrPT7p35DgNR27aw/UserResultByIdQuery"
|
graphUserTweetsAndRepliesV2* = "BDX77Xzqypdt11-mDfgdpQ/UserWithProfileTweetsAndRepliesQueryV2"
|
||||||
graphUserTweetsV2* = gql / "6QdSuZ5feXxOadEdXa4XZg/UserWithProfileTweetsQueryV2"
|
graphUserTweets* = "oRJs8SLCRNRbQzuZG93_oA/UserTweets"
|
||||||
graphUserTweetsAndRepliesV2* = gql / "BDX77Xzqypdt11-mDfgdpQ/UserWithProfileTweetsAndRepliesQueryV2"
|
graphUserTweetsAndReplies* = "kkaJ0Mf34PZVarrxzLihjg/UserTweetsAndReplies"
|
||||||
graphUserTweets* = gql / "oRJs8SLCRNRbQzuZG93_oA/UserTweets"
|
graphUserMedia* = "36oKqyQ7E_9CmtONGjJRsA/UserMedia"
|
||||||
graphUserTweetsAndReplies* = gql / "kkaJ0Mf34PZVarrxzLihjg/UserTweetsAndReplies"
|
graphUserMediaV2* = "bp0e_WdXqgNBIwlLukzyYA/MediaTimelineV2"
|
||||||
graphUserMedia* = gql / "36oKqyQ7E_9CmtONGjJRsA/UserMedia"
|
graphTweet* = "Y4Erk_-0hObvLpz0Iw3bzA/ConversationTimeline"
|
||||||
graphUserMediaV2* = gql / "bp0e_WdXqgNBIwlLukzyYA/MediaTimelineV2"
|
graphTweetDetail* = "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"
|
||||||
graphTweet* = gql / "Y4Erk_-0hObvLpz0Iw3bzA/ConversationTimeline"
|
graphTweetResult* = "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"
|
||||||
graphTweetDetail* = gql / "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"
|
graphSearchTimeline* = "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"
|
||||||
graphTweetResult* = gql / "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"
|
graphListById* = "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"
|
||||||
graphSearchTimeline* = gql / "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"
|
graphListBySlug* = "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"
|
||||||
graphListById* = gql / "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"
|
graphListMembers* = "fuVHh5-gFn8zDBBxb8wOMA/ListMembers"
|
||||||
graphListBySlug* = gql / "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"
|
graphListTweets* = "VQf8_XQynI3WzH6xopOMMQ/ListTimeline"
|
||||||
graphListMembers* = gql / "fuVHh5-gFn8zDBBxb8wOMA/ListMembers"
|
|
||||||
graphListTweets* = gql / "VQf8_XQynI3WzH6xopOMMQ/ListTimeline"
|
|
||||||
|
|
||||||
gqlFeatures* = """{
|
gqlFeatures* = """{
|
||||||
"android_ad_formats_media_component_render_overlay_enabled": false,
|
"android_ad_formats_media_component_render_overlay_enabled": false,
|
||||||
|
|||||||
8
src/experimental/parser/tid.nim
Normal file
8
src/experimental/parser/tid.nim
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import jsony
|
||||||
|
import ../types/tid
|
||||||
|
export TidPair
|
||||||
|
|
||||||
|
proc parseTidPairs*(raw: string): seq[TidPair] =
|
||||||
|
result = raw.fromJson(seq[TidPair])
|
||||||
|
if result.len == 0:
|
||||||
|
raise newException(ValueError, "Parsing pairs failed: " & raw)
|
||||||
4
src/experimental/types/tid.nim
Normal file
4
src/experimental/types/tid.nim
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
type
|
||||||
|
TidPair* = object
|
||||||
|
animationKey*: string
|
||||||
|
verification*: string
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import strutils, strformat, times, uri, tables, xmltree, htmlparser, htmlgen
|
import strutils, strformat, times, uri, tables, xmltree, htmlparser, htmlgen, math
|
||||||
import std/[enumerate, re]
|
import std/[enumerate, re]
|
||||||
import types, utils, query
|
import types, utils, query
|
||||||
|
|
||||||
@@ -154,6 +154,17 @@ proc getShortTime*(tweet: Tweet): string =
|
|||||||
else:
|
else:
|
||||||
result = "now"
|
result = "now"
|
||||||
|
|
||||||
|
proc getDuration*(video: Video): string =
|
||||||
|
let
|
||||||
|
ms = video.durationMs
|
||||||
|
sec = int(round(ms / 1000))
|
||||||
|
min = floorDiv(sec, 60)
|
||||||
|
hour = floorDiv(min, 60)
|
||||||
|
if hour > 0:
|
||||||
|
return &"{hour}:{min mod 60}:{sec mod 60:02}"
|
||||||
|
else:
|
||||||
|
return &"{min mod 60}:{sec mod 60:02}"
|
||||||
|
|
||||||
proc getLink*(tweet: Tweet; focus=true): string =
|
proc getLink*(tweet: Tweet; focus=true): string =
|
||||||
if tweet.id == 0: return
|
if tweet.id == 0: return
|
||||||
var username = tweet.user.username
|
var username = tweet.user.username
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from os import getEnv
|
|||||||
|
|
||||||
import jester
|
import jester
|
||||||
|
|
||||||
import types, config, prefs, formatters, redis_cache, http_pool, auth
|
import types, config, prefs, formatters, redis_cache, http_pool, auth, apiutils
|
||||||
import views/[general, about]
|
import views/[general, about]
|
||||||
import routes/[
|
import routes/[
|
||||||
preferences, timeline, status, media, search, rss, list, debug,
|
preferences, timeline, status, media, search, rss, list, debug,
|
||||||
@@ -37,6 +37,7 @@ setHmacKey(cfg.hmacKey)
|
|||||||
setProxyEncoding(cfg.base64Media)
|
setProxyEncoding(cfg.base64Media)
|
||||||
setMaxHttpConns(cfg.httpMaxConns)
|
setMaxHttpConns(cfg.httpMaxConns)
|
||||||
setHttpProxy(cfg.proxy, cfg.proxyAuth)
|
setHttpProxy(cfg.proxy, cfg.proxyAuth)
|
||||||
|
setDisableTid(cfg.disableTid)
|
||||||
initAboutPage(cfg.staticDir)
|
initAboutPage(cfg.staticDir)
|
||||||
|
|
||||||
waitFor initRedisPool(cfg)
|
waitFor initRedisPool(cfg)
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ proc parseMediaEntities(js: JsonNode; result: var Tweet) =
|
|||||||
# Remove media URLs from text
|
# Remove media URLs from text
|
||||||
with mediaList, js{"legacy", "entities", "media"}:
|
with mediaList, js{"legacy", "entities", "media"}:
|
||||||
for url in mediaList:
|
for url in mediaList:
|
||||||
let expandedUrl = url{"expanded_url"}.getStr
|
let expandedUrl = url.getExpandedUrl
|
||||||
if result.text.endsWith(expandedUrl):
|
if result.text.endsWith(expandedUrl):
|
||||||
result.text.removeSuffix(expandedUrl)
|
result.text.removeSuffix(expandedUrl)
|
||||||
result.text = result.text.strip()
|
result.text = result.text.strip()
|
||||||
@@ -267,7 +267,7 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card =
|
|||||||
|
|
||||||
for u in ? urls:
|
for u in ? urls:
|
||||||
if u{"url"}.getStr == result.url:
|
if u{"url"}.getStr == result.url:
|
||||||
result.url = u{"expanded_url"}.getStr
|
result.url = u.getExpandedUrl(result.url)
|
||||||
break
|
break
|
||||||
|
|
||||||
if kind in {videoDirectMessage, imageDirectMessage}:
|
if kind in {videoDirectMessage, imageDirectMessage}:
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ proc getImageStr*(js: JsonNode): string =
|
|||||||
template getImageVal*(js: JsonNode): string =
|
template getImageVal*(js: JsonNode): string =
|
||||||
js{"image_value", "url"}.getImageStr
|
js{"image_value", "url"}.getImageStr
|
||||||
|
|
||||||
|
template getExpandedUrl*(js: JsonNode; fallback=""): string =
|
||||||
|
js{"expanded_url"}.getStr(js{"url"}.getStr(fallback))
|
||||||
|
|
||||||
proc getCardUrl*(js: JsonNode; kind: CardKind): string =
|
proc getCardUrl*(js: JsonNode; kind: CardKind): string =
|
||||||
result = js{"website_url"}.getStrVal
|
result = js{"website_url"}.getStrVal
|
||||||
if kind == promoVideoConvo:
|
if kind == promoVideoConvo:
|
||||||
@@ -177,7 +180,7 @@ proc extractSlice(js: JsonNode): Slice[int] =
|
|||||||
proc extractUrls(result: var seq[ReplaceSlice]; js: JsonNode;
|
proc extractUrls(result: var seq[ReplaceSlice]; js: JsonNode;
|
||||||
textLen: int; hideTwitter = false) =
|
textLen: int; hideTwitter = false) =
|
||||||
let
|
let
|
||||||
url = js["expanded_url"].getStr
|
url = js.getExpandedUrl
|
||||||
slice = js.extractSlice
|
slice = js.extractSlice
|
||||||
|
|
||||||
if hideTwitter and slice.b.succ >= textLen and url.isTwitterUrl:
|
if hideTwitter and slice.b.succ >= textLen and url.isTwitterUrl:
|
||||||
@@ -238,7 +241,7 @@ proc expandUserEntities*(user: var User; js: JsonNode) =
|
|||||||
ent = ? js{"entities"}
|
ent = ? js{"entities"}
|
||||||
|
|
||||||
with urls, ent{"url", "urls"}:
|
with urls, ent{"url", "urls"}:
|
||||||
user.website = urls[0]{"expanded_url"}.getStr
|
user.website = urls[0].getExpandedUrl
|
||||||
|
|
||||||
var replacements = newSeq[ReplaceSlice]()
|
var replacements = newSeq[ReplaceSlice]()
|
||||||
|
|
||||||
@@ -268,7 +271,7 @@ proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlic
|
|||||||
replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink)
|
replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink)
|
||||||
|
|
||||||
if hasCard and u{"url"}.getStr == get(tweet.card).url:
|
if hasCard and u{"url"}.getStr == get(tweet.card).url:
|
||||||
get(tweet.card).url = u{"expanded_url"}.getStr
|
get(tweet.card).url = u.getExpandedUrl
|
||||||
|
|
||||||
with media, entities{"media"}:
|
with media, entities{"media"}:
|
||||||
for m in media:
|
for m in media:
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
.panel-container {
|
.panel-container {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-panel {
|
.error-panel {
|
||||||
@include center-panel(var(--error_red));
|
@include center-panel(var(--error_red));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar > form {
|
.search-bar > form {
|
||||||
@include center-panel(var(--darkest_grey));
|
@include center-panel(var(--darkest_grey));
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: var(--bg_elements);
|
background: var(--bg_elements);
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
}
|
padding: 0px 5px 1px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--bg_elements);
|
background: var(--bg_elements);
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,43 @@
|
|||||||
// colors
|
// colors
|
||||||
$bg_color: #0F0F0F;
|
$bg_color: #0f0f0f;
|
||||||
$fg_color: #F8F8F2;
|
$fg_color: #f8f8f2;
|
||||||
$fg_faded: #F8F8F2CF;
|
$fg_faded: #f8f8f2cf;
|
||||||
$fg_dark: #FF6C60;
|
$fg_dark: #ff6c60;
|
||||||
$fg_nav: #FF6C60;
|
$fg_nav: #ff6c60;
|
||||||
|
|
||||||
$bg_panel: #161616;
|
$bg_panel: #161616;
|
||||||
$bg_elements: #121212;
|
$bg_elements: #121212;
|
||||||
$bg_overlays: #1F1F1F;
|
$bg_overlays: #1f1f1f;
|
||||||
$bg_hover: #1A1A1A;
|
$bg_hover: #1a1a1a;
|
||||||
|
|
||||||
$grey: #888889;
|
$grey: #888889;
|
||||||
$dark_grey: #404040;
|
$dark_grey: #404040;
|
||||||
$darker_grey: #282828;
|
$darker_grey: #282828;
|
||||||
$darkest_grey: #222222;
|
$darkest_grey: #222222;
|
||||||
$border_grey: #3E3E35;
|
$border_grey: #3e3e35;
|
||||||
|
|
||||||
$accent: #FF6C60;
|
$accent: #ff6c60;
|
||||||
$accent_light: #FFACA0;
|
$accent_light: #ffaca0;
|
||||||
$accent_dark: #8A3731;
|
$accent_dark: #8a3731;
|
||||||
$accent_border: #FF6C6091;
|
$accent_border: #ff6c6091;
|
||||||
|
|
||||||
$play_button: #D8574D;
|
$play_button: #d8574d;
|
||||||
$play_button_hover: #FF6C60;
|
$play_button_hover: #ff6c60;
|
||||||
|
|
||||||
$more_replies_dots: #AD433B;
|
$more_replies_dots: #ad433b;
|
||||||
$error_red: #420A05;
|
$error_red: #420a05;
|
||||||
|
|
||||||
$verified_blue: #1DA1F2;
|
$verified_blue: #1da1f2;
|
||||||
$verified_business: #FAC82B;
|
$verified_business: #fac82b;
|
||||||
$verified_government: #C1B6A4;
|
$verified_government: #c1b6a4;
|
||||||
$icon_text: $fg_color;
|
$icon_text: $fg_color;
|
||||||
|
|
||||||
$tab: $fg_color;
|
$tab: $fg_color;
|
||||||
$tab_selected: $accent;
|
$tab_selected: $accent;
|
||||||
|
|
||||||
$shadow: rgba(0,0,0,.6);
|
$shadow: rgba(0, 0, 0, 0.6);
|
||||||
$shadow_dark: rgba(0,0,0,.2);
|
$shadow_dark: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
//fonts
|
//fonts
|
||||||
$font_0: Helvetica Neue;
|
$font_0: sans-serif;
|
||||||
$font_1: Helvetica;
|
$font_1: fontello;
|
||||||
$font_2: Arial;
|
|
||||||
$font_3: sans-serif;
|
|
||||||
$font_4: fontello;
|
|
||||||
|
|||||||
@@ -1,180 +1,202 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
@import 'tweet/_base';
|
@import "tweet/_base";
|
||||||
@import 'profile/_base';
|
@import "profile/_base";
|
||||||
@import 'general';
|
@import "general";
|
||||||
@import 'navbar';
|
@import "navbar";
|
||||||
@import 'inputs';
|
@import "inputs";
|
||||||
@import 'timeline';
|
@import "timeline";
|
||||||
@import 'search';
|
@import "search";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// colors
|
// colors
|
||||||
--bg_color: #{$bg_color};
|
--bg_color: #{$bg_color};
|
||||||
--fg_color: #{$fg_color};
|
--fg_color: #{$fg_color};
|
||||||
--fg_faded: #{$fg_faded};
|
--fg_faded: #{$fg_faded};
|
||||||
--fg_dark: #{$fg_dark};
|
--fg_dark: #{$fg_dark};
|
||||||
--fg_nav: #{$fg_nav};
|
--fg_nav: #{$fg_nav};
|
||||||
|
|
||||||
--bg_panel: #{$bg_panel};
|
--bg_panel: #{$bg_panel};
|
||||||
--bg_elements: #{$bg_elements};
|
--bg_elements: #{$bg_elements};
|
||||||
--bg_overlays: #{$bg_overlays};
|
--bg_overlays: #{$bg_overlays};
|
||||||
--bg_hover: #{$bg_hover};
|
--bg_hover: #{$bg_hover};
|
||||||
|
|
||||||
--grey: #{$grey};
|
--grey: #{$grey};
|
||||||
--dark_grey: #{$dark_grey};
|
--dark_grey: #{$dark_grey};
|
||||||
--darker_grey: #{$darker_grey};
|
--darker_grey: #{$darker_grey};
|
||||||
--darkest_grey: #{$darkest_grey};
|
--darkest_grey: #{$darkest_grey};
|
||||||
--border_grey: #{$border_grey};
|
--border_grey: #{$border_grey};
|
||||||
|
|
||||||
--accent: #{$accent};
|
--accent: #{$accent};
|
||||||
--accent_light: #{$accent_light};
|
--accent_light: #{$accent_light};
|
||||||
--accent_dark: #{$accent_dark};
|
--accent_dark: #{$accent_dark};
|
||||||
--accent_border: #{$accent_border};
|
--accent_border: #{$accent_border};
|
||||||
|
|
||||||
--play_button: #{$play_button};
|
--play_button: #{$play_button};
|
||||||
--play_button_hover: #{$play_button_hover};
|
--play_button_hover: #{$play_button_hover};
|
||||||
|
|
||||||
--more_replies_dots: #{$more_replies_dots};
|
--more_replies_dots: #{$more_replies_dots};
|
||||||
--error_red: #{$error_red};
|
--error_red: #{$error_red};
|
||||||
|
|
||||||
--verified_blue: #{$verified_blue};
|
--verified_blue: #{$verified_blue};
|
||||||
--verified_business: #{$verified_business};
|
--verified_business: #{$verified_business};
|
||||||
--verified_government: #{$verified_government};
|
--verified_government: #{$verified_government};
|
||||||
--icon_text: #{$icon_text};
|
--icon_text: #{$icon_text};
|
||||||
|
|
||||||
--tab: #{$fg_color};
|
--tab: #{$fg_color};
|
||||||
--tab_selected: #{$accent};
|
--tab_selected: #{$accent};
|
||||||
|
|
||||||
--profile_stat: #{$fg_color};
|
--profile_stat: #{$fg_color};
|
||||||
|
|
||||||
background-color: var(--bg_color);
|
background-color: var(--bg_color);
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
font-family: $font_0, $font_1, $font_2, $font_3;
|
font-family: $font_0, $font_1;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
outline: unset;
|
outline: unset;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2, h3 {
|
h2,
|
||||||
font-weight: normal;
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 14px 0;
|
margin: 14px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: -0.6em;
|
margin-top: -0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: .6em 0 .3em 0;
|
padding: 0.6em 0 0.3em 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-bottom: 1px solid var(--border_grey);
|
border-bottom: 1px solid var(--border_grey);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preferences .note {
|
.preferences .note {
|
||||||
border-top: 1px solid var(--border_grey);
|
border-top: 1px solid var(--border_grey);
|
||||||
border-bottom: 1px solid var(--border_grey);
|
border-bottom: 1px solid var(--border_grey);
|
||||||
padding: 6px 0 8px 0;
|
padding: 6px 0 8px 0;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left: 1.3em;
|
padding-left: 1.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-panel {
|
.overlay-panel {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
background-color: var(--bg_overlays);
|
background-color: var(--bg_overlays);
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.verified-icon {
|
.verified-icon {
|
||||||
color: var(--icon_text);
|
display: inline-block;
|
||||||
border-radius: 50%;
|
width: 14px;
|
||||||
flex-shrink: 0;
|
height: 14px;
|
||||||
margin: 2px 0 3px 3px;
|
margin-left: 2px;
|
||||||
padding-top: 3px;
|
|
||||||
height: 11px;
|
|
||||||
width: 14px;
|
|
||||||
font-size: 8px;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&.blue {
|
.verified-icon-circle {
|
||||||
background-color: var(--verified_blue);
|
position: absolute;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-icon-check {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 9px;
|
||||||
|
margin: 5px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blue {
|
||||||
|
.verified-icon-circle {
|
||||||
|
color: var(--verified_blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.business {
|
.verified-icon-check {
|
||||||
color: var(--bg_panel);
|
color: var(--icon_text);
|
||||||
background-color: var(--verified_business);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.business {
|
||||||
|
.verified-icon-circle {
|
||||||
|
color: var(--verified_business);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.government {
|
.verified-icon-check {
|
||||||
color: var(--bg_panel);
|
color: var(--bg_panel);
|
||||||
background-color: var(--verified_government);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.government {
|
||||||
|
.verified-icon-circle {
|
||||||
|
color: var(--verified_government);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-icon-check {
|
||||||
|
color: var(--bg_panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.preferences-container {
|
.preferences-container {
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item, .nav-item .icon-container {
|
.nav-item,
|
||||||
font-size: 16px;
|
.nav-item .icon-container {
|
||||||
}
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +1,203 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include input-colors;
|
@include input-colors;
|
||||||
background-color: var(--bg_elements);
|
background-color: var(--bg_elements);
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
border: 1px solid var(--accent_border);
|
border: 1px solid var(--accent_border);
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="date"],
|
input[type="date"],
|
||||||
input[type="number"],
|
input[type="number"],
|
||||||
select {
|
select {
|
||||||
@include input-colors;
|
@include input-colors;
|
||||||
background-color: var(--bg_elements);
|
background-color: var(--bg_elements);
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
border: 1px solid var(--accent_border);
|
border: 1px solid var(--accent_border);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="date"]::-webkit-inner-spin-button {
|
input[type="date"]::-webkit-inner-spin-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"]::-webkit-inner-spin-button,
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
input[type="number"]::-webkit-outer-spin-button {
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
display: none;
|
display: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="date"]::-webkit-clear-button {
|
input[type="date"]::-webkit-clear-button {
|
||||||
margin-left: 17px;
|
margin-left: 17px;
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
filter: hue-rotate(120deg);
|
filter: hue-rotate(120deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-calendar-picker-indicator {
|
input::-webkit-calendar-picker-indicator {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::-webkit-datetime-edit-day-field:focus,
|
input::-webkit-datetime-edit-day-field:focus,
|
||||||
input::-webkit-datetime-edit-month-field:focus,
|
input::-webkit-datetime-edit-month-field:focus,
|
||||||
input::-webkit-datetime-edit-year-field:focus {
|
input::-webkit-datetime-edit-year-field:focus {
|
||||||
background-color: var(--accent);
|
background-color: var(--accent);
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-range {
|
.date-range {
|
||||||
.date-input {
|
.date-input {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-title {
|
.search-title {
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button button {
|
.icon-button button {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
float: none;
|
float: none;
|
||||||
padding: unset;
|
padding: unset;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent_light);
|
color: var(--accent_light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
width: 17px;
|
width: 17px;
|
||||||
background-color: var(--bg_elements);
|
background-color: var(--bg_elements);
|
||||||
border: 1px solid var(--accent_border);
|
border: 1px solid var(--accent_border);
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding-right: 22px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
height: 0;
|
||||||
padding-right: 22px;
|
width: 0;
|
||||||
|
|
||||||
input {
|
&:checked ~ .checkbox:after {
|
||||||
position: absolute;
|
display: block;
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
|
|
||||||
&:checked ~ .checkbox:after {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover input ~ .checkbox {
|
&:hover input ~ .checkbox {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active input ~ .checkbox {
|
&:active input ~ .checkbox {
|
||||||
border-color: var(--accent_light);
|
border-color: var(--accent_light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox:after {
|
.checkbox:after {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-family: $font_4;
|
font-family: $font_1;
|
||||||
content: '\e803';
|
content: "\e811";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-group {
|
.pref-group {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preferences {
|
.preferences {
|
||||||
button {
|
button {
|
||||||
margin: 6px 0 3px 0;
|
margin: 6px 0 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
padding-right: 150px;
|
padding-right: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
display: block;
|
display: block;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-group {
|
.pref-group {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-input {
|
.pref-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pref-reset {
|
.pref-reset {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,87 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: var(--bg_overlays);
|
background-color: var(--bg_overlays);
|
||||||
box-shadow: 0 0 4px $shadow;
|
box-shadow: 0 0 4px $shadow;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
a, .icon-button button {
|
a,
|
||||||
color: var(--fg_nav);
|
.icon-button button {
|
||||||
}
|
color: var(--fg_nav);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inner-nav {
|
.inner-nav {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-basis: 920px;
|
flex-basis: 920px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-name {
|
.site-name {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent_light);
|
color: var(--accent_light);
|
||||||
text-decoration: unset;
|
text-decoration: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-logo {
|
.site-logo {
|
||||||
display: block;
|
display: block;
|
||||||
width: 35px;
|
width: 35px;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right a {
|
&.right a:hover {
|
||||||
padding-left: 4px;
|
color: var(--accent_light);
|
||||||
|
text-decoration: unset;
|
||||||
&:hover {
|
}
|
||||||
color: var(--accent_light);
|
|
||||||
text-decoration: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lp {
|
.lp {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
fill: var(--fg_nav);
|
fill: var(--fg_nav);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
fill: var(--accent_light);
|
fill: var(--accent_light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-info:before {
|
.icon-info {
|
||||||
margin: 0 -3px;
|
margin: 0 -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-cog {
|
.icon-cog {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
padding-left: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,120 +1,118 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
.search-title {
|
.search-title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-field {
|
.search-field {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0 2px 0 0;
|
||||||
|
padding: 0px 1px 1px 4px;
|
||||||
|
height: 23px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
.pref-input {
|
||||||
margin: 0 2px 0 0;
|
margin: 0 4px 0 0;
|
||||||
height: 23px;
|
flex-grow: 1;
|
||||||
display: flex;
|
height: 23px;
|
||||||
align-items: center;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.pref-input {
|
input[type="text"],
|
||||||
margin: 0 4px 0 0;
|
input[type="number"] {
|
||||||
flex-grow: 1;
|
height: calc(100% - 4px);
|
||||||
height: 23px;
|
width: calc(100% - 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"],
|
> label {
|
||||||
input[type="number"] {
|
display: inline;
|
||||||
height: calc(100% - 4px);
|
background-color: var(--bg_elements);
|
||||||
width: calc(100% - 8px);
|
color: var(--fg_color);
|
||||||
}
|
border: 1px solid var(--accent_border);
|
||||||
|
padding: 1px 1px 2px 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
> label {
|
@include input-colors;
|
||||||
display: inline;
|
}
|
||||||
background-color: var(--bg_elements);
|
|
||||||
color: var(--fg_color);
|
|
||||||
border: 1px solid var(--accent_border);
|
|
||||||
padding: 1px 6px 2px 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
|
|
||||||
@include input-colors;
|
@include create-toggle(search-panel, 380px);
|
||||||
}
|
|
||||||
|
|
||||||
@include create-toggle(search-panel, 380px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-panel {
|
.search-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.4s;
|
transition: max-height 0.4s;
|
||||||
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-weight: initial;
|
font-weight: initial;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
> div {
|
.checkbox-container {
|
||||||
line-height: 1.7em;
|
display: inline;
|
||||||
}
|
padding-right: unset;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox-container {
|
.checkbox {
|
||||||
display: inline;
|
right: unset;
|
||||||
padding-right: unset;
|
left: -22px;
|
||||||
margin-bottom: unset;
|
line-height: 1.6em;
|
||||||
margin-left: 23px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
.checkbox-container .checkbox:after {
|
||||||
right: unset;
|
top: -4px;
|
||||||
left: -22px;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-container .checkbox:after {
|
|
||||||
top: -4px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-row {
|
.search-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pref-input {
|
||||||
|
display: block;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
height: 21px;
|
height: 21px;
|
||||||
}
|
margin-top: 1px;
|
||||||
|
|
||||||
.pref-input {
|
|
||||||
display: block;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
height: 21px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-toggles {
|
.search-toggles {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, auto);
|
grid-template-columns: repeat(5, auto);
|
||||||
grid-column-gap: 10px;
|
grid-column-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-tabs {
|
.profile-tabs {
|
||||||
@include search-resize(820px, 5);
|
@include search-resize(820px, 5);
|
||||||
@include search-resize(715px, 4);
|
@include search-resize(715px, 4);
|
||||||
@include search-resize(700px, 5);
|
@include search-resize(700px, 5);
|
||||||
@include search-resize(485px, 4);
|
@include search-resize(485px, 4);
|
||||||
@include search-resize(410px, 3);
|
@include search-resize(410px, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include search-resize(700px, 5);
|
@include search-resize(700px, 5);
|
||||||
|
|||||||
@@ -1,162 +1,162 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
@include panel(100%, 600px);
|
@include panel(100%, 600px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
|
|
||||||
> div:not(:first-child) {
|
> div:not(:first-child) {
|
||||||
border-top: 1px solid var(--border_grey);
|
border-top: 1px solid var(--border_grey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-header {
|
.timeline-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
float: unset;
|
float: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-banner img {
|
.timeline-banner img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-description {
|
.timeline-description {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
border-bottom: .1rem solid transparent;
|
border-bottom: 0.1rem solid transparent;
|
||||||
color: var(--tab);
|
color: var(--tab);
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-bottom-color: var(--tab_selected);
|
|
||||||
color: var(--tab_selected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active a {
|
&.active {
|
||||||
border-bottom-color: var(--tab_selected);
|
border-bottom-color: var(--tab_selected);
|
||||||
color: var(--tab_selected);
|
color: var(--tab_selected);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.wide {
|
&.active a {
|
||||||
flex-grow: 1.2;
|
border-bottom-color: var(--tab_selected);
|
||||||
flex-basis: 50px;
|
color: var(--tab_selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.wide {
|
||||||
|
flex-grow: 1.2;
|
||||||
|
flex-basis: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-footer {
|
.timeline-footer {
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-protected {
|
.timeline-protected {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: var(--accent);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-none {
|
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-none {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-end {
|
.timeline-end {
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-more {
|
.show-more {
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: .75em 0;
|
padding: 0.75em 0;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
background-color: var(--darkest_grey);
|
background-color: var(--darkest_grey);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--darker_grey);
|
background-color: var(--darker_grey);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-ref {
|
.top-ref {
|
||||||
background-color: var(--bg_color);
|
background-color: var(--bg_color);
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
|
|
||||||
.icon-down {
|
.icon-down {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--accent_light);
|
color: var(--accent_light);
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
transform: rotate(180deg) translateY(-1px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: rotate(180deg) translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item {
|
.timeline-item {
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
border-left-width: 0;
|
border-left-width: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: .75em;
|
padding: 0.75em;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,240 +1,244 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
@import 'thread';
|
@import "thread";
|
||||||
@import 'media';
|
@import "media";
|
||||||
@import 'video';
|
@import "video";
|
||||||
@import 'embed';
|
@import "embed";
|
||||||
@import 'card';
|
@import "card";
|
||||||
@import 'poll';
|
@import "poll";
|
||||||
@import 'quote';
|
@import "quote";
|
||||||
|
|
||||||
.tweet-body {
|
.tweet-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-left: 58px;
|
margin-left: 58px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-content {
|
.tweet-content {
|
||||||
font-family: $font_3;
|
line-height: 1.3em;
|
||||||
line-height: 1.3em;
|
pointer-events: all;
|
||||||
pointer-events: all;
|
display: inline;
|
||||||
display: inline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-bidi {
|
.tweet-bidi {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-header {
|
.tweet-header {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
margin-bottom: .2em;
|
margin-bottom: 0.2em;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-name-row {
|
.tweet-name-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullname-and-username {
|
.fullname-and-username {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullname {
|
.fullname {
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
flex-shrink: 2;
|
flex-shrink: 2;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
min-width: 1.6em;
|
min-width: 1.6em;
|
||||||
margin-left: .4em;
|
margin-left: 0.4em;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-date {
|
.tweet-date {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-date a, .username, .show-more a {
|
.tweet-date a,
|
||||||
color: var(--fg_dark);
|
.username,
|
||||||
|
.show-more a {
|
||||||
|
color: var(--fg_dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-published {
|
.tweet-published {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: var(--grey);
|
color: var(--grey);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-avatar {
|
.tweet-avatar {
|
||||||
display: contents !important;
|
display: contents !important;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
float: left;
|
float: left;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
margin-left: -58px;
|
margin-left: -58px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
&.round {
|
&.round {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mini {
|
&.mini {
|
||||||
position: unset;
|
position: unset;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-embed {
|
.tweet-embed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--bg_panel);
|
||||||
|
|
||||||
|
.tweet-content {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
max-height: calc(100vh - 0.75em * 2);
|
||||||
height: 100%;
|
}
|
||||||
background-color: var(--bg_panel);
|
|
||||||
|
|
||||||
.tweet-content {
|
.card-image img {
|
||||||
font-size: 18px;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: calc(100vh - 0.75em * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-image img {
|
.avatar {
|
||||||
height: auto;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribution {
|
.attribution {
|
||||||
display: flex;
|
display: flex;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
color: var(--fg_color);
|
color: var(--fg_color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-tag-block {
|
.media-tag-block {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
color: var(--fg_faded);
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-tag,
|
||||||
|
.icon-container {
|
||||||
color: var(--fg_faded);
|
color: var(--fg_faded);
|
||||||
|
}
|
||||||
.icon-container {
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-tag, .icon-container {
|
|
||||||
color: var(--fg_faded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-container .media-tag-block {
|
.timeline-container .media-tag-block {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-geo {
|
.tweet-geo {
|
||||||
color: var(--fg_faded);
|
color: var(--fg_faded);
|
||||||
}
|
}
|
||||||
|
|
||||||
.replying-to {
|
.replying-to {
|
||||||
color: var(--fg_faded);
|
color: var(--fg_faded);
|
||||||
margin: -2px 0 4px;
|
margin: -2px 0 4px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.retweet-header, .pinned, .tweet-stats {
|
.retweet-header,
|
||||||
align-content: center;
|
.pinned,
|
||||||
color: var(--grey);
|
.tweet-stats {
|
||||||
display: flex;
|
align-content: center;
|
||||||
flex-shrink: 0;
|
color: var(--grey);
|
||||||
flex-wrap: wrap;
|
display: flex;
|
||||||
font-size: 14px;
|
flex-shrink: 0;
|
||||||
font-weight: 600;
|
flex-wrap: wrap;
|
||||||
line-height: 22px;
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 22px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.retweet-header {
|
.retweet-header {
|
||||||
margin-top: -5px !important;
|
margin-top: -5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-stats {
|
.tweet-stats {
|
||||||
margin-bottom: -3px;
|
margin-bottom: -3px;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-stat {
|
.tweet-stat {
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
min-width: 1em;
|
min-width: 1em;
|
||||||
margin-right: 0.8em;
|
margin-right: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-thread {
|
.show-thread {
|
||||||
display: block;
|
display: block;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unavailable-box {
|
.unavailable-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: solid 1px var(--dark_grey);
|
border: solid 1px var(--dark_grey);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: var(--bg_color);
|
background-color: var(--bg_color);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tweet-link {
|
.tweet-link {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg_hover);
|
background-color: var(--bg_hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
.embed-video {
|
.embed-video {
|
||||||
.gallery-video {
|
.gallery-video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
top: 0%;
|
top: 0%;
|
||||||
left: 0%;
|
left: 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
max-height: unset;
|
max-height: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
.gallery-row {
|
.gallery-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-height: 379.5px;
|
max-height: 379.5px;
|
||||||
max-width: 533px;
|
max-width: 533px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
.still-image {
|
.still-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments {
|
.attachments {
|
||||||
margin-top: .35em;
|
margin-top: 0.35em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
background-color: var(--bg_color);
|
background-color: var(--bg_color);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|
||||||
.image-attachment {
|
.image-attachment {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment {
|
.attachment {
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 .25em 0 0;
|
margin: 0 0.25em 0 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 2em;
|
min-width: 2em;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-height: 530px;
|
max-height: 530px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-gif video {
|
.gallery-gif video {
|
||||||
max-height: 530px;
|
max-height: 530px;
|
||||||
background-color: #101010;
|
background-color: #101010;
|
||||||
}
|
}
|
||||||
|
|
||||||
.still-image {
|
.still-image {
|
||||||
max-height: 379.5px;
|
max-height: 379.5px;
|
||||||
max-width: 533px;
|
max-width: 533px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 379.5px;
|
max-height: 379.5px;
|
||||||
flex-basis: 300px;
|
flex-basis: 300px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .single-image {
|
// .single-image {
|
||||||
@@ -86,34 +86,34 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
.overlay-circle {
|
.overlay-circle {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--dark_grey);
|
background-color: var(--dark_grey);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
border-width: 5px;
|
border-width: 5px;
|
||||||
border-color: var(--play_button);
|
border-color: var(--play_button);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-triangle {
|
.overlay-triangle {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 12px 0 12px 17px;
|
border-width: 12px 0 12px 17px;
|
||||||
border-color: transparent transparent transparent var(--play_button);
|
border-color: transparent transparent transparent var(--play_button);
|
||||||
margin-left: 14px;
|
margin-left: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-gif {
|
.media-gif {
|
||||||
display: table;
|
display: table;
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
width: unset;
|
width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-body {
|
.media-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
.poll-meter {
|
.poll-meter {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
background: var(--bg_color);
|
background: var(--bg_color);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-choice-bar {
|
.poll-choice-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: var(--dark_grey);
|
background: var(--dark_grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-choice-value {
|
.poll-choice-value {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-choice-option {
|
.poll-choice-option {
|
||||||
position: relative;
|
position: relative;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-info {
|
.poll-info {
|
||||||
color: var(--grey);
|
color: var(--grey);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leader .poll-choice-bar {
|
.leader .poll-choice-bar {
|
||||||
background: var(--accent_dark);
|
background: var(--accent_dark);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +1,95 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
|
|
||||||
.quote {
|
.quote {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border: solid 1px var(--dark_grey);
|
border: solid 1px var(--dark_grey);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: var(--bg_elements);
|
background-color: var(--bg_elements);
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: all;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unavailable:hover {
|
||||||
|
border-color: var(--dark_grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-name-row {
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-text {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: all;
|
white-space: pre-wrap;
|
||||||
position: relative;
|
word-wrap: break-word;
|
||||||
width: 100%;
|
padding: 0px 8px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
.show-thread {
|
||||||
border-color: var(--grey);
|
padding: 0px 8px 6px 8px;
|
||||||
}
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
&.unavailable:hover {
|
.replying-to {
|
||||||
border-color: var(--dark_grey);
|
padding: 0px 8px;
|
||||||
}
|
margin: unset;
|
||||||
|
}
|
||||||
.tweet-name-row {
|
|
||||||
padding: 6px 8px;
|
|
||||||
margin-top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote-text {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
padding: 0px 8px 8px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-thread {
|
|
||||||
padding: 0px 8px 6px 8px;
|
|
||||||
margin-top: -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.replying-to {
|
|
||||||
padding: 0px 8px;
|
|
||||||
margin: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.unavailable-quote {
|
.unavailable-quote {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-link {
|
.quote-link {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote-media-container {
|
.quote-media-container {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachments {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-gif {
|
||||||
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.gallery-gif .attachment {
|
||||||
margin: unset;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--bg_color);
|
||||||
|
|
||||||
|
video {
|
||||||
|
height: unset;
|
||||||
|
width: unset;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.attachments {
|
.gallery-video,
|
||||||
border-radius: 0;
|
.gallery-gif {
|
||||||
}
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
.media-gif {
|
.still-image img {
|
||||||
width: 100%;
|
max-height: 250px;
|
||||||
display: flex;
|
}
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-gif .attachment {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: var(--bg_color);
|
|
||||||
|
|
||||||
video {
|
|
||||||
height: unset;
|
|
||||||
width: unset;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-video, .gallery-gif {
|
|
||||||
max-height: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.still-image img {
|
|
||||||
max-height: 250px
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,138 +1,139 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
.conversation {
|
.conversation {
|
||||||
@include panel(100%, 600px);
|
@include panel(100%, 600px);
|
||||||
|
|
||||||
.show-more {
|
.show-more {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-thread {
|
.main-thread {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-tweet, .replies {
|
.main-tweet,
|
||||||
padding-top: 50px;
|
.replies {
|
||||||
margin-top: -50px;
|
padding-top: 50px;
|
||||||
|
margin-top: -50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-tweet .tweet-content {
|
.main-tweet .tweet-content {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.main-tweet .tweet-content {
|
.main-tweet .tweet-content {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply {
|
.reply {
|
||||||
background-color: var(--bg_panel);
|
background-color: var(--bg_panel);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-line {
|
.thread-line {
|
||||||
.timeline-item::before,
|
.timeline-item::before,
|
||||||
&.timeline-item::before {
|
&.timeline-item::before {
|
||||||
background: var(--accent_dark);
|
background: var(--accent_dark);
|
||||||
content: '';
|
content: "";
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 3px;
|
min-width: 3px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
left: 26px;
|
left: 26px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
margin-left: -3px;
|
margin-left: -3px;
|
||||||
margin-bottom: 37px;
|
margin-bottom: 37px;
|
||||||
top: 56px;
|
top: 56px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-header:not(:first-child)::after {
|
.with-header:not(:first-child)::after {
|
||||||
background: var(--accent_dark);
|
background: var(--accent_dark);
|
||||||
content: '';
|
content: "";
|
||||||
position: relative;
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
min-width: 3px;
|
min-width: 3px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
right: calc(100% - 26px);
|
right: calc(100% - 26px);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
margin-left: -3px;
|
margin-left: -3px;
|
||||||
margin-bottom: 37px;
|
margin-bottom: 37px;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unavailable::before {
|
.unavailable::before {
|
||||||
top: 48px;
|
top: 48px;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-replies::before {
|
.more-replies::before {
|
||||||
content: '...';
|
content: "...";
|
||||||
background: unset;
|
background: unset;
|
||||||
color: var(--more_replies_dots);
|
color: var(--more_replies_dots);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 0.25em;
|
line-height: 0.25em;
|
||||||
left: 1.2em;
|
left: 1.2em;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-left: -2.5px;
|
margin-left: -2.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.earlier-replies {
|
.earlier-replies {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
margin-bottom: -5px;
|
margin-bottom: -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item.thread-last::before {
|
.timeline-item.thread-last::before {
|
||||||
background: unset;
|
background: unset;
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
width: 0;
|
width: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-replies {
|
.more-replies {
|
||||||
padding-top: 0.3em !important;
|
padding-top: 0.3em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-replies-text {
|
.more-replies-text {
|
||||||
@include ellipsis;
|
@include ellipsis;
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: 58px;
|
margin-left: 58px;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-item.thread.more-replies-thread {
|
.timeline-item.thread.more-replies-thread {
|
||||||
padding: 0 0.75em;
|
padding: 0 0.75em;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 40px;
|
||||||
|
margin-bottom: 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-replies {
|
||||||
|
display: flex;
|
||||||
|
padding-top: unset !important;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
top: 40px;
|
display: inline-block;
|
||||||
margin-bottom: 31px;
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
line-height: 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-replies {
|
.more-replies-text {
|
||||||
display: flex;
|
display: inline;
|
||||||
padding-top: unset !important;
|
|
||||||
margin-top: 8px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
line-height: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-replies-text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,77 @@
|
|||||||
@import '_variables';
|
@import "_variables";
|
||||||
@import '_mixins';
|
@import "_mixins";
|
||||||
|
|
||||||
video {
|
video {
|
||||||
max-height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-video {
|
.gallery-video {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-video.card-container {
|
.gallery-video.card-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-height: 530px;
|
max-height: 530px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-overlay {
|
.video-overlay {
|
||||||
@include play-button;
|
@include play-button;
|
||||||
background-color: $shadow;
|
background-color: $shadow;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
top: calc(50% - 20px);
|
top: calc(50% - 20px);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
.overlay-circle {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
top: calc(50% - 20px);
|
top: calc(50% - 20px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
.overlay-duration {
|
||||||
width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
bottom: 8px;
|
||||||
align-items: center;
|
left: 8px;
|
||||||
justify-content: center;
|
background-color: #0000007a;
|
||||||
display: flex;
|
line-height: 1em;
|
||||||
}
|
padding: 4px 6px 4px 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
form {
|
||||||
padding: 5px 8px;
|
width: 100%;
|
||||||
font-size: 16px;
|
height: 100%;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/tid.nim
Normal file
62
src/tid.nim
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import std/[asyncdispatch, base64, httpclient, random, strutils, sequtils, times]
|
||||||
|
import nimcrypto
|
||||||
|
import experimental/parser/tid
|
||||||
|
|
||||||
|
randomize()
|
||||||
|
|
||||||
|
const defaultKeyword = "obfiowerehiring";
|
||||||
|
const pairsUrl =
|
||||||
|
"https://raw.githubusercontent.com/fa0311/x-client-transaction-id-pair-dict/refs/heads/main/pair.json";
|
||||||
|
|
||||||
|
var
|
||||||
|
cachedPairs: seq[TidPair] = @[]
|
||||||
|
lastCached = 0
|
||||||
|
# refresh every hour
|
||||||
|
ttlSec = 60 * 60
|
||||||
|
|
||||||
|
proc getPair(): Future[TidPair] {.async.} =
|
||||||
|
if cachedPairs.len == 0 or int(epochTime()) - lastCached > ttlSec:
|
||||||
|
lastCached = int(epochTime())
|
||||||
|
|
||||||
|
let client = newAsyncHttpClient()
|
||||||
|
defer: client.close()
|
||||||
|
|
||||||
|
let resp = await client.get(pairsUrl)
|
||||||
|
if resp.status == $Http200:
|
||||||
|
cachedPairs = parseTidPairs(await resp.body)
|
||||||
|
|
||||||
|
return sample(cachedPairs)
|
||||||
|
|
||||||
|
proc encodeSha256(text: string): array[32, byte] =
|
||||||
|
let
|
||||||
|
data = cast[ptr byte](addr text[0])
|
||||||
|
dataLen = uint(len(text))
|
||||||
|
digest = sha256.digest(data, dataLen)
|
||||||
|
return digest.data
|
||||||
|
|
||||||
|
proc encodeBase64[T](data: T): string =
|
||||||
|
return encode(data).replace("=", "")
|
||||||
|
|
||||||
|
proc decodeBase64(data: string): seq[byte] =
|
||||||
|
return cast[seq[byte]](decode(data))
|
||||||
|
|
||||||
|
proc genTid*(path: string): Future[string] {.async.} =
|
||||||
|
let
|
||||||
|
pair = await getPair()
|
||||||
|
|
||||||
|
timeNow = int(epochTime() - 1682924400)
|
||||||
|
timeNowBytes = @[
|
||||||
|
byte(timeNow and 0xff),
|
||||||
|
byte((timeNow shr 8) and 0xff),
|
||||||
|
byte((timeNow shr 16) and 0xff),
|
||||||
|
byte((timeNow shr 24) and 0xff)
|
||||||
|
]
|
||||||
|
|
||||||
|
data = "GET!" & path & "!" & $timeNow & defaultKeyword & pair.animationKey
|
||||||
|
hashBytes = encodeSha256(data)
|
||||||
|
keyBytes = decodeBase64(pair.verification)
|
||||||
|
bytesArr = keyBytes & timeNowBytes & hashBytes[0 ..< 16] & @[3'u8]
|
||||||
|
randomNum = byte(rand(256))
|
||||||
|
tid = @[randomNum] & bytesArr.mapIt(it xor randomNum)
|
||||||
|
|
||||||
|
return encodeBase64(tid)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import times, sequtils, options, tables, uri
|
import times, sequtils, options, tables
|
||||||
import prefs_impl
|
import prefs_impl
|
||||||
|
|
||||||
genPrefsType()
|
genPrefsType()
|
||||||
@@ -13,19 +13,13 @@ type
|
|||||||
TimelineKind* {.pure.} = enum
|
TimelineKind* {.pure.} = enum
|
||||||
tweets, replies, media
|
tweets, replies, media
|
||||||
|
|
||||||
Api* {.pure.} = enum
|
ApiUrl* = object
|
||||||
tweetDetail
|
endpoint*: string
|
||||||
tweetResult
|
params*: seq[(string, string)]
|
||||||
search
|
|
||||||
list
|
ApiReq* = object
|
||||||
listBySlug
|
oauth*: ApiUrl
|
||||||
listMembers
|
cookie*: ApiUrl
|
||||||
listTweets
|
|
||||||
userRestId
|
|
||||||
userScreenName
|
|
||||||
userTweets
|
|
||||||
userTweetsAndReplies
|
|
||||||
userMedia
|
|
||||||
|
|
||||||
RateLimit* = object
|
RateLimit* = object
|
||||||
limit*: int
|
limit*: int
|
||||||
@@ -42,7 +36,7 @@ type
|
|||||||
pending*: int
|
pending*: int
|
||||||
limited*: bool
|
limited*: bool
|
||||||
limitedAt*: int
|
limitedAt*: int
|
||||||
apis*: Table[Api, RateLimit]
|
apis*: Table[string, RateLimit]
|
||||||
case kind*: SessionKind
|
case kind*: SessionKind
|
||||||
of oauth:
|
of oauth:
|
||||||
oauthToken*: string
|
oauthToken*: string
|
||||||
@@ -51,10 +45,6 @@ type
|
|||||||
authToken*: string
|
authToken*: string
|
||||||
ct0*: string
|
ct0*: string
|
||||||
|
|
||||||
SessionAwareUrl* = object
|
|
||||||
oauthUrl*: Uri
|
|
||||||
cookieUrl*: Uri
|
|
||||||
|
|
||||||
Error* = enum
|
Error* = enum
|
||||||
null = 0
|
null = 0
|
||||||
noUserMatches = 17
|
noUserMatches = 17
|
||||||
@@ -285,6 +275,7 @@ type
|
|||||||
enableDebug*: bool
|
enableDebug*: bool
|
||||||
proxy*: string
|
proxy*: string
|
||||||
proxyAuth*: string
|
proxyAuth*: string
|
||||||
|
disableTid*: bool
|
||||||
|
|
||||||
rssCacheTime*: int
|
rssCacheTime*: int
|
||||||
listCacheTime*: int
|
listCacheTime*: int
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
|||||||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||||
|
|
||||||
buildHtml(head):
|
buildHtml(head):
|
||||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=21")
|
link(rel="stylesheet", type="text/css", href="/css/style.css?v=22")
|
||||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=3")
|
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||||
|
|
||||||
if theme.len > 0:
|
if theme.len > 0:
|
||||||
link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css"))
|
link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css"))
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode =
|
|||||||
template verifiedIcon*(user: User): untyped {.dirty.} =
|
template verifiedIcon*(user: User): untyped {.dirty.} =
|
||||||
if user.verifiedType != VerifiedType.none:
|
if user.verifiedType != VerifiedType.none:
|
||||||
let lower = ($user.verifiedType).toLowerAscii()
|
let lower = ($user.verifiedType).toLowerAscii()
|
||||||
icon "ok", class=(&"verified-icon {lower}"), title=(&"Verified {lower} account")
|
buildHtml(tdiv(class=(&"verified-icon {lower}"))):
|
||||||
|
icon "circle", class="verified-icon-circle", title=(&"Verified {lower} account")
|
||||||
|
icon "ok", class="verified-icon-check", title=(&"Verified {lower} account")
|
||||||
else:
|
else:
|
||||||
text ""
|
text ""
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode =
|
|||||||
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos)
|
||||||
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
verbatim "<div class=\"video-overlay\" onclick=\"playVideo(this)\">"
|
||||||
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
tdiv(class="overlay-circle"): span(class="overlay-triangle")
|
||||||
|
tdiv(class="overlay-duration"): text getDuration(video)
|
||||||
verbatim "</div>"
|
verbatim "</div>"
|
||||||
if container.len > 0:
|
if container.len > 0:
|
||||||
tdiv(class="card-content"):
|
tdiv(class="card-content"):
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ card = [
|
|||||||
['voidtarget/status/1094632512926605312',
|
['voidtarget/status/1094632512926605312',
|
||||||
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too)',
|
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too)',
|
||||||
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too) - obsplugin.nim',
|
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too) - obsplugin.nim',
|
||||||
'gist.github.com', True],
|
'gist.github.com', True]
|
||||||
|
|
||||||
['nim_lang/status/1082989146040340480',
|
|
||||||
'Nim in 2018: A short recap',
|
|
||||||
'There were several big news in the Nim world in 2018 – two new major releases, partnership with Status, and much more. But let us go chronologically.',
|
|
||||||
'nim-lang.org', True]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
no_thumb = [
|
no_thumb = [
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ async def login_and_get_cookies(username, password, totp_seed=None, headless=Fal
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
if len(sys.argv) < 3:
|
if len(sys.argv) < 3:
|
||||||
print('Usage: python3 twitter-auth.py username password [totp_seed] [--append sessions.jsonl] [--headless]')
|
print('Usage: python3 create_session_browser.py username password [totp_seed] [--append file.jsonl] [--headless]')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
username = sys.argv[1]
|
username = sys.argv[1]
|
||||||
|
|||||||
Reference in New Issue
Block a user