マージ
このコミットが含まれているのは:
コミット
2ee1602423
|
@ -4,10 +4,13 @@ EXPOSE 8080
|
||||||
|
|
||||||
RUN apk --no-cache add libsass-dev pcre
|
RUN apk --no-cache add libsass-dev pcre
|
||||||
|
|
||||||
COPY . /src/nitter
|
|
||||||
WORKDIR /src/nitter
|
WORKDIR /src/nitter
|
||||||
|
|
||||||
RUN nimble build -y -d:danger -d:lto -d:strip \
|
COPY nitter.nimble .
|
||||||
|
RUN nimble install -y --depsOnly
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN nimble build -d:danger -d:lto -d:strip \
|
||||||
&& nimble scss \
|
&& nimble scss \
|
||||||
&& nimble md
|
&& nimble md
|
||||||
|
|
||||||
|
|
12
src/api.nim
12
src/api.nim
|
@ -2,14 +2,14 @@
|
||||||
import asyncdispatch, httpclient, uri, strutils, sequtils, sugar
|
import asyncdispatch, httpclient, uri, strutils, sequtils, sugar
|
||||||
import packedjson
|
import packedjson
|
||||||
import types, query, formatters, consts, apiutils, parser
|
import types, query, formatters, consts, apiutils, parser
|
||||||
import experimental/parser/user
|
import experimental/parser as newParser
|
||||||
|
|
||||||
proc getGraphUser*(id: string): Future[User] {.async.} =
|
proc getGraphUser*(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
|
||||||
variables = %*{"userId": id, "withSuperFollowsUserFields": true}
|
variables = %*{"userId": id, "withSuperFollowsUserFields": true}
|
||||||
js = await fetch(graphUser ? {"variables": $variables}, Api.userRestId)
|
js = await fetchRaw(graphUser ? {"variables": $variables}, Api.userRestId)
|
||||||
result = parseGraphUser(js, id)
|
result = parseGraphUser(js)
|
||||||
|
|
||||||
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
|
proc getGraphListBySlug*(name, list: string): Future[List] {.async.} =
|
||||||
let
|
let
|
||||||
|
@ -37,7 +37,7 @@ proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.}
|
||||||
"withSuperFollowsTweetFields": false
|
"withSuperFollowsTweetFields": false
|
||||||
}
|
}
|
||||||
url = graphListMembers ? {"variables": $variables}
|
url = graphListMembers ? {"variables": $variables}
|
||||||
result = parseGraphListMembers(await fetch(url, Api.listMembers), after)
|
result = parseGraphListMembers(await fetchRaw(url, Api.listMembers), after)
|
||||||
|
|
||||||
proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} =
|
proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} =
|
||||||
if id.len == 0: return
|
if id.len == 0: return
|
||||||
|
@ -85,10 +85,12 @@ proc getSearch*[T](query: Query; after=""): Future[Result[T]] {.async.} =
|
||||||
const
|
const
|
||||||
searchMode = ("result_filter", "user")
|
searchMode = ("result_filter", "user")
|
||||||
parse = parseUsers
|
parse = parseUsers
|
||||||
|
fetchFunc = fetchRaw
|
||||||
else:
|
else:
|
||||||
const
|
const
|
||||||
searchMode = ("tweet_search_mode", "live")
|
searchMode = ("tweet_search_mode", "live")
|
||||||
parse = parseTimeline
|
parse = parseTimeline
|
||||||
|
fetchFunc = fetch
|
||||||
|
|
||||||
let q = genQueryParam(query)
|
let q = genQueryParam(query)
|
||||||
if q.len == 0 or q == emptyQuery:
|
if q.len == 0 or q == emptyQuery:
|
||||||
|
@ -96,7 +98,7 @@ proc getSearch*[T](query: Query; after=""): Future[Result[T]] {.async.} =
|
||||||
|
|
||||||
let url = search ? genParams(searchParams & @[("q", q), searchMode], after)
|
let url = search ? genParams(searchParams & @[("q", q), searchMode], after)
|
||||||
try:
|
try:
|
||||||
result = parse(await fetch(url, Api.search), after)
|
result = parse(await fetchFunc(url, Api.search), after)
|
||||||
result.query = query
|
result.query = query
|
||||||
except InternalError:
|
except InternalError:
|
||||||
return Result[T](beginning: true, query: query)
|
return Result[T](beginning: true, query: query)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import parser/[user, graphql, timeline]
|
||||||
|
export user, graphql, timeline
|
|
@ -0,0 +1,27 @@
|
||||||
|
import jsony
|
||||||
|
import user, ../types/[graphuser, graphlistmembers]
|
||||||
|
from ../../types import User, Result, Query, QueryKind
|
||||||
|
|
||||||
|
proc parseGraphUser*(json: string): User =
|
||||||
|
let raw = json.fromJson(GraphUser)
|
||||||
|
result = toUser raw.data.user.result.legacy
|
||||||
|
result.id = raw.data.user.result.restId
|
||||||
|
|
||||||
|
proc parseGraphListMembers*(json, cursor: string): Result[User] =
|
||||||
|
result = Result[User](
|
||||||
|
beginning: cursor.len == 0,
|
||||||
|
query: Query(kind: userList)
|
||||||
|
)
|
||||||
|
|
||||||
|
let raw = json.fromJson(GraphListMembers)
|
||||||
|
for instruction in raw.data.list.membersTimeline.timeline.instructions:
|
||||||
|
if instruction.kind == "TimelineAddEntries":
|
||||||
|
for entry in instruction.entries:
|
||||||
|
case entry.content.entryType
|
||||||
|
of TimelineTimelineItem:
|
||||||
|
let userResult = entry.content.itemContent.userResults.result
|
||||||
|
if userResult.restId.len > 0:
|
||||||
|
result.content.add toUser userResult.legacy
|
||||||
|
of TimelineTimelineCursor:
|
||||||
|
if entry.content.cursorType == "Bottom":
|
||||||
|
result.bottom = entry.content.value
|
|
@ -0,0 +1,28 @@
|
||||||
|
import std/[strutils, tables]
|
||||||
|
import jsony
|
||||||
|
import user, ../types/timeline
|
||||||
|
from ../../types import Result, User
|
||||||
|
|
||||||
|
proc getId(id: string): string {.inline.} =
|
||||||
|
let start = id.rfind("-")
|
||||||
|
if start < 0: return id
|
||||||
|
id[start + 1 ..< id.len]
|
||||||
|
|
||||||
|
proc parseUsers*(json: string; after=""): Result[User] =
|
||||||
|
result = Result[User](beginning: after.len == 0)
|
||||||
|
|
||||||
|
let raw = json.fromJson(Search)
|
||||||
|
if raw.timeline.instructions.len == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for e in raw.timeline.instructions[0].addEntries.entries:
|
||||||
|
let id = e.entryId.getId
|
||||||
|
if e.entryId.startsWith("user"):
|
||||||
|
if id in raw.globalObjects.users:
|
||||||
|
result.content.add toUser raw.globalObjects.users[id]
|
||||||
|
elif e.entryId.startsWith("cursor"):
|
||||||
|
let cursor = e.content.operation.cursor
|
||||||
|
if cursor.cursorType == "Top":
|
||||||
|
result.top = cursor.value
|
||||||
|
elif cursor.cursorType == "Bottom":
|
||||||
|
result.bottom = cursor.value
|
|
@ -1,4 +1,4 @@
|
||||||
import std/[algorithm, unicode, re, strutils]
|
import std/[algorithm, unicode, re, strutils, strformat, options]
|
||||||
import jsony
|
import jsony
|
||||||
import utils, slices
|
import utils, slices
|
||||||
import ../types/user as userType
|
import ../types/user as userType
|
||||||
|
@ -34,9 +34,40 @@ proc expandUserEntities(user: var User; raw: RawUser) =
|
||||||
proc getBanner(user: RawUser): string =
|
proc getBanner(user: RawUser): string =
|
||||||
if user.profileBannerUrl.len > 0:
|
if user.profileBannerUrl.len > 0:
|
||||||
return user.profileBannerUrl & "/1500x500"
|
return user.profileBannerUrl & "/1500x500"
|
||||||
|
|
||||||
if user.profileLinkColor.len > 0:
|
if user.profileLinkColor.len > 0:
|
||||||
return '#' & user.profileLinkColor
|
return '#' & user.profileLinkColor
|
||||||
|
|
||||||
|
if user.profileImageExtensions.isSome:
|
||||||
|
let ext = get(user.profileImageExtensions)
|
||||||
|
if ext.mediaColor.r.ok.palette.len > 0:
|
||||||
|
let color = ext.mediaColor.r.ok.palette[0].rgb
|
||||||
|
return &"#{color.red:02x}{color.green:02x}{color.blue:02x}"
|
||||||
|
|
||||||
|
proc toUser*(raw: RawUser): User =
|
||||||
|
result = User(
|
||||||
|
id: raw.idStr,
|
||||||
|
username: raw.screenName,
|
||||||
|
fullname: raw.name,
|
||||||
|
location: raw.location,
|
||||||
|
bio: raw.description,
|
||||||
|
following: raw.friendsCount,
|
||||||
|
followers: raw.followersCount,
|
||||||
|
tweets: raw.statusesCount,
|
||||||
|
likes: raw.favouritesCount,
|
||||||
|
media: raw.mediaCount,
|
||||||
|
verified: raw.verified,
|
||||||
|
protected: raw.protected,
|
||||||
|
joinDate: parseTwitterDate(raw.createdAt),
|
||||||
|
banner: getBanner(raw),
|
||||||
|
userPic: getImageUrl(raw.profileImageUrlHttps).replace("_normal", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
if raw.pinnedTweetIdsStr.len > 0:
|
||||||
|
result.pinnedTweet = parseBiggestInt(raw.pinnedTweetIdsStr[0])
|
||||||
|
|
||||||
|
result.expandUserEntities(raw)
|
||||||
|
|
||||||
proc parseUser*(json: string; username=""): User =
|
proc parseUser*(json: string; username=""): User =
|
||||||
handleErrors:
|
handleErrors:
|
||||||
case error.code
|
case error.code
|
||||||
|
@ -44,24 +75,4 @@ proc parseUser*(json: string; username=""): User =
|
||||||
of userNotFound: return
|
of userNotFound: return
|
||||||
else: echo "[error - parseUser]: ", error
|
else: echo "[error - parseUser]: ", error
|
||||||
|
|
||||||
let user = json.fromJson(RawUser)
|
result = toUser json.fromJson(RawUser)
|
||||||
|
|
||||||
result = User(
|
|
||||||
id: user.idStr,
|
|
||||||
username: user.screenName,
|
|
||||||
fullname: user.name,
|
|
||||||
location: user.location,
|
|
||||||
bio: user.description,
|
|
||||||
following: user.friendsCount,
|
|
||||||
followers: user.followersCount,
|
|
||||||
tweets: user.statusesCount,
|
|
||||||
likes: user.favouritesCount,
|
|
||||||
media: user.mediaCount,
|
|
||||||
verified: user.verified,
|
|
||||||
protected: user.protected,
|
|
||||||
joinDate: parseTwitterDate(user.createdAt),
|
|
||||||
banner: getBanner(user),
|
|
||||||
userPic: getImageUrl(user.profileImageUrlHttps).replace("_normal", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
result.expandUserEntities(user)
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import graphuser
|
||||||
|
|
||||||
|
type
|
||||||
|
GraphListMembers* = object
|
||||||
|
data*: tuple[list: List]
|
||||||
|
|
||||||
|
List = object
|
||||||
|
membersTimeline*: tuple[timeline: Timeline]
|
||||||
|
|
||||||
|
Timeline = object
|
||||||
|
instructions*: seq[Instruction]
|
||||||
|
|
||||||
|
Instruction = object
|
||||||
|
kind*: string
|
||||||
|
entries*: seq[tuple[content: Content]]
|
||||||
|
|
||||||
|
ContentEntryType* = enum
|
||||||
|
TimelineTimelineItem
|
||||||
|
TimelineTimelineCursor
|
||||||
|
|
||||||
|
Content = object
|
||||||
|
case entryType*: ContentEntryType
|
||||||
|
of TimelineTimelineItem:
|
||||||
|
itemContent*: tuple[userResults: UserData]
|
||||||
|
of TimelineTimelineCursor:
|
||||||
|
value*: string
|
||||||
|
cursorType*: string
|
||||||
|
|
||||||
|
proc renameHook*(v: var Instruction; fieldName: var string) =
|
||||||
|
if fieldName == "type":
|
||||||
|
fieldName = "kind"
|
|
@ -0,0 +1,12 @@
|
||||||
|
import user
|
||||||
|
|
||||||
|
type
|
||||||
|
GraphUser* = object
|
||||||
|
data*: tuple[user: UserData]
|
||||||
|
|
||||||
|
UserData* = object
|
||||||
|
result*: UserResult
|
||||||
|
|
||||||
|
UserResult = object
|
||||||
|
legacy*: RawUser
|
||||||
|
restId*: string
|
|
@ -0,0 +1,23 @@
|
||||||
|
import std/tables
|
||||||
|
import user
|
||||||
|
|
||||||
|
type
|
||||||
|
Search* = object
|
||||||
|
globalObjects*: GlobalObjects
|
||||||
|
timeline*: Timeline
|
||||||
|
|
||||||
|
GlobalObjects = object
|
||||||
|
users*: Table[string, RawUser]
|
||||||
|
|
||||||
|
Timeline = object
|
||||||
|
instructions*: seq[Instructions]
|
||||||
|
|
||||||
|
Instructions = object
|
||||||
|
addEntries*: tuple[entries: seq[Entry]]
|
||||||
|
|
||||||
|
Entry = object
|
||||||
|
entryId*: string
|
||||||
|
content*: tuple[operation: Operation]
|
||||||
|
|
||||||
|
Operation = object
|
||||||
|
cursor*: tuple[value, cursorType: string]
|
|
@ -1,3 +1,4 @@
|
||||||
|
import options
|
||||||
import common
|
import common
|
||||||
|
|
||||||
type
|
type
|
||||||
|
@ -16,9 +17,11 @@ type
|
||||||
mediaCount*: int
|
mediaCount*: int
|
||||||
verified*: bool
|
verified*: bool
|
||||||
protected*: bool
|
protected*: bool
|
||||||
|
profileLinkColor*: string
|
||||||
profileBannerUrl*: string
|
profileBannerUrl*: string
|
||||||
profileImageUrlHttps*: string
|
profileImageUrlHttps*: string
|
||||||
profileLinkColor*: string
|
profileImageExtensions*: Option[ImageExtensions]
|
||||||
|
pinnedTweetIdsStr*: seq[string]
|
||||||
|
|
||||||
Entities* = object
|
Entities* = object
|
||||||
url*: Urls
|
url*: Urls
|
||||||
|
@ -26,3 +29,15 @@ type
|
||||||
|
|
||||||
Urls* = object
|
Urls* = object
|
||||||
urls*: seq[Url]
|
urls*: seq[Url]
|
||||||
|
|
||||||
|
ImageExtensions = object
|
||||||
|
mediaColor*: tuple[r: Ok]
|
||||||
|
|
||||||
|
Ok = object
|
||||||
|
ok*: Palette
|
||||||
|
|
||||||
|
Palette = object
|
||||||
|
palette*: seq[tuple[rgb: Color]]
|
||||||
|
|
||||||
|
Color* = object
|
||||||
|
red*, green*, blue*: int
|
||||||
|
|
|
@ -26,16 +26,6 @@ proc parseUser(js: JsonNode; id=""): User =
|
||||||
|
|
||||||
result.expandUserEntities(js)
|
result.expandUserEntities(js)
|
||||||
|
|
||||||
proc parseGraphUser*(js: JsonNode; id: string): User =
|
|
||||||
if js.isNull: return
|
|
||||||
|
|
||||||
with user, js{"data", "user", "result", "legacy"}:
|
|
||||||
result = parseUser(user, id)
|
|
||||||
|
|
||||||
with pinned, user{"pinned_tweet_ids_str"}:
|
|
||||||
if pinned.kind == JArray and pinned.len > 0:
|
|
||||||
result.pinnedTweet = parseBiggestInt(pinned[0].getStr)
|
|
||||||
|
|
||||||
proc parseGraphList*(js: JsonNode): List =
|
proc parseGraphList*(js: JsonNode): List =
|
||||||
if js.isNull: return
|
if js.isNull: return
|
||||||
|
|
||||||
|
@ -55,25 +45,6 @@ proc parseGraphList*(js: JsonNode): List =
|
||||||
banner: list{"custom_banner_media", "media_info", "url"}.getImageStr
|
banner: list{"custom_banner_media", "media_info", "url"}.getImageStr
|
||||||
)
|
)
|
||||||
|
|
||||||
proc parseGraphListMembers*(js: JsonNode; cursor: string): Result[User] =
|
|
||||||
result = Result[User](
|
|
||||||
beginning: cursor.len == 0,
|
|
||||||
query: Query(kind: userList)
|
|
||||||
)
|
|
||||||
|
|
||||||
if js.isNull: return
|
|
||||||
|
|
||||||
let root = js{"data", "list", "members_timeline", "timeline", "instructions"}
|
|
||||||
for instruction in root:
|
|
||||||
if instruction{"type"}.getStr == "TimelineAddEntries":
|
|
||||||
for entry in instruction{"entries"}:
|
|
||||||
let content = entry{"content"}
|
|
||||||
if content{"entryType"}.getStr == "TimelineTimelineItem":
|
|
||||||
with legacy, content{"itemContent", "user_results", "result", "legacy"}:
|
|
||||||
result.content.add parseUser(legacy)
|
|
||||||
elif content{"cursorType"}.getStr == "Bottom":
|
|
||||||
result.bottom = content{"value"}.getStr
|
|
||||||
|
|
||||||
|
|
||||||
proc parsePoll(js: JsonNode): Poll =
|
proc parsePoll(js: JsonNode): Poll =
|
||||||
let vals = js{"binding_values"}
|
let vals = js{"binding_values"}
|
||||||
|
@ -395,26 +366,6 @@ proc parseInstructions[T](res: var Result[T]; global: GlobalObjects; js: JsonNod
|
||||||
elif "bottom" in r{"entryId"}.getStr:
|
elif "bottom" in r{"entryId"}.getStr:
|
||||||
res.bottom = r.getCursor
|
res.bottom = r.getCursor
|
||||||
|
|
||||||
proc parseUsers*(js: JsonNode; after=""): Result[User] =
|
|
||||||
result = Result[User](beginning: after.len == 0)
|
|
||||||
let global = parseGlobalObjects(? js)
|
|
||||||
|
|
||||||
let instructions = ? js{"timeline", "instructions"}
|
|
||||||
if instructions.len == 0: return
|
|
||||||
|
|
||||||
result.parseInstructions(global, instructions)
|
|
||||||
|
|
||||||
for e in instructions[0]{"addEntries", "entries"}:
|
|
||||||
let entry = e{"entryId"}.getStr
|
|
||||||
if "user-" in entry:
|
|
||||||
let id = entry.getId
|
|
||||||
if id in global.users:
|
|
||||||
result.content.add global.users[id]
|
|
||||||
elif "cursor-top" in entry:
|
|
||||||
result.top = e.getCursor
|
|
||||||
elif "cursor-bottom" in entry:
|
|
||||||
result.bottom = e.getCursor
|
|
||||||
|
|
||||||
proc parseTimeline*(js: JsonNode; after=""): Timeline =
|
proc parseTimeline*(js: JsonNode; after=""): Timeline =
|
||||||
result = Timeline(beginning: after.len == 0)
|
result = Timeline(beginning: after.len == 0)
|
||||||
let global = parseGlobalObjects(? js)
|
let global = parseGlobalObjects(? js)
|
||||||
|
|
|
@ -8,7 +8,7 @@ let
|
||||||
unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})"
|
unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})"
|
||||||
unReplace = "$1<a href=\"/$2\">@$2</a>"
|
unReplace = "$1<a href=\"/$2\">@$2</a>"
|
||||||
|
|
||||||
htRegex = re"(^|[^\w-_./?])([##$])([\w_]+)"
|
htRegex = re"(^|[^\w-_./?])([#$]|#)([\w_]+)"
|
||||||
htReplace = "$1<a href=\"/search?q=%23$3\">$2$3</a>"
|
htReplace = "$1<a href=\"/search?q=%23$3\">$2$3</a>"
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
|
@ -102,8 +102,9 @@ proc cache*(data: Tweet) {.async.} =
|
||||||
proc cacheRss*(query: string; rss: Rss) {.async.} =
|
proc cacheRss*(query: string; rss: Rss) {.async.} =
|
||||||
let key = "rss:" & query
|
let key = "rss:" & query
|
||||||
pool.withAcquire(r):
|
pool.withAcquire(r):
|
||||||
dawait r.hSet(key, "rss", rss.feed)
|
|
||||||
dawait r.hSet(key, "min", rss.cursor)
|
dawait r.hSet(key, "min", rss.cursor)
|
||||||
|
if rss.cursor != "suspended":
|
||||||
|
dawait r.hSet(key, "rss", compress(rss.feed))
|
||||||
dawait r.expire(key, rssCacheTime)
|
dawait r.expire(key, rssCacheTime)
|
||||||
|
|
||||||
template deserialize(data, T) =
|
template deserialize(data, T) =
|
||||||
|
@ -182,6 +183,10 @@ proc getCachedRss*(key: string): Future[Rss] {.async.} =
|
||||||
pool.withAcquire(r):
|
pool.withAcquire(r):
|
||||||
result.cursor = await r.hGet(k, "min")
|
result.cursor = await r.hGet(k, "min")
|
||||||
if result.cursor.len > 2:
|
if result.cursor.len > 2:
|
||||||
result.feed = await r.hGet(k, "rss")
|
if result.cursor != "suspended":
|
||||||
|
let feed = await r.hGet(k, "rss")
|
||||||
|
if feed.len > 0 and feed != redisNil:
|
||||||
|
try: result.feed = uncompress feed
|
||||||
|
except: echo "Decompressing RSS failed: ", feed
|
||||||
else:
|
else:
|
||||||
result.cursor.setLen 0
|
result.cursor.setLen 0
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import asyncdispatch, strutils, tables, times, hashes, uri
|
import asyncdispatch, strutils, tables, times, hashes, uri
|
||||||
|
|
||||||
import jester, supersnappy
|
import jester
|
||||||
|
|
||||||
import router_utils, timeline
|
import router_utils, timeline
|
||||||
import ../query
|
import ../query
|
||||||
|
|
||||||
include "../views/rss.nimf"
|
include "../views/rss.nimf"
|
||||||
|
|
||||||
export times, hashes, supersnappy
|
export times, hashes
|
||||||
|
|
||||||
proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.} =
|
proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.} =
|
||||||
var profile: Profile
|
var profile: Profile
|
||||||
|
@ -36,7 +36,7 @@ proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.
|
||||||
return Rss(feed: profile.user.username, cursor: "suspended")
|
return Rss(feed: profile.user.username, cursor: "suspended")
|
||||||
|
|
||||||
if profile.user.fullname.len > 0:
|
if profile.user.fullname.len > 0:
|
||||||
let rss = compress renderTimelineRss(profile, cfg, multi=(names.len > 1))
|
let rss = renderTimelineRss(profile, cfg, multi=(names.len > 1))
|
||||||
return Rss(feed: rss, cursor: profile.tweets.bottom)
|
return Rss(feed: rss, cursor: profile.tweets.bottom)
|
||||||
|
|
||||||
template respRss*(rss, page) =
|
template respRss*(rss, page) =
|
||||||
|
@ -48,11 +48,11 @@ template respRss*(rss, page) =
|
||||||
|
|
||||||
resp Http404, showError(page & info & "not found", cfg)
|
resp Http404, showError(page & info & "not found", cfg)
|
||||||
elif rss.cursor.len == 9 and rss.cursor == "suspended":
|
elif rss.cursor.len == 9 and rss.cursor == "suspended":
|
||||||
resp Http404, showError(getSuspended(rss.feed), cfg)
|
resp Http404, showError(getSuspended(@"name"), cfg)
|
||||||
|
|
||||||
let headers = {"Content-Type": "application/rss+xml; charset=utf-8",
|
let headers = {"Content-Type": "application/rss+xml; charset=utf-8",
|
||||||
"Min-Id": rss.cursor}
|
"Min-Id": rss.cursor}
|
||||||
resp Http200, headers, uncompress rss.feed
|
resp Http200, headers, rss.feed
|
||||||
|
|
||||||
proc createRssRouter*(cfg: Config) =
|
proc createRssRouter*(cfg: Config) =
|
||||||
router rss:
|
router rss:
|
||||||
|
@ -75,8 +75,7 @@ proc createRssRouter*(cfg: Config) =
|
||||||
|
|
||||||
let tweets = await getSearch[Tweet](query, cursor)
|
let tweets = await getSearch[Tweet](query, cursor)
|
||||||
rss.cursor = tweets.bottom
|
rss.cursor = tweets.bottom
|
||||||
rss.feed = compress renderSearchRss(tweets.content, query.text,
|
rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg)
|
||||||
genQueryUrl(query), cfg)
|
|
||||||
|
|
||||||
await cacheRss(key, rss)
|
await cacheRss(key, rss)
|
||||||
respRss(rss, "Search")
|
respRss(rss, "Search")
|
||||||
|
@ -157,7 +156,7 @@ proc createRssRouter*(cfg: Config) =
|
||||||
list = await getCachedList(id=(@"id"))
|
list = await getCachedList(id=(@"id"))
|
||||||
timeline = await getListTimeline(list.id, cursor)
|
timeline = await getListTimeline(list.id, cursor)
|
||||||
rss.cursor = timeline.bottom
|
rss.cursor = timeline.bottom
|
||||||
rss.feed = compress renderListRss(timeline.content, list, cfg)
|
rss.feed = renderListRss(timeline.content, list, cfg)
|
||||||
|
|
||||||
await cacheRss(key, rss)
|
await cacheRss(key, rss)
|
||||||
respRss(rss, "List")
|
respRss(rss, "List")
|
||||||
|
|
|
@ -86,7 +86,7 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
||||||
html = renderTweetSearch(timeline, prefs, getPath())
|
html = renderTweetSearch(timeline, prefs, getPath())
|
||||||
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
|
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
|
||||||
|
|
||||||
var profile = await fetchProfile(after, query)
|
var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins)
|
||||||
template u: untyped = profile.user
|
template u: untyped = profile.user
|
||||||
|
|
||||||
if u.suspended:
|
if u.suspended:
|
||||||
|
|
|
@ -42,12 +42,16 @@
|
||||||
top: 50px;
|
top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-result .username {
|
.profile-result {
|
||||||
margin: 0 !important;
|
min-height: 54px;
|
||||||
}
|
|
||||||
|
|
||||||
.profile-result .tweet-header {
|
.username {
|
||||||
margin-bottom: unset;
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-header {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width: 700px) {
|
@media(max-width: 700px) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ 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=15")
|
link(rel="stylesheet", type="text/css", href="/css/style.css?v=16")
|
||||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
|
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
|
||||||
|
|
||||||
if theme.len > 0:
|
if theme.len > 0:
|
||||||
|
|
|
@ -35,7 +35,7 @@ macro renderPrefs*(): untyped =
|
||||||
proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode =
|
proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode =
|
||||||
buildHtml(tdiv(class="overlay-panel")):
|
buildHtml(tdiv(class="overlay-panel")):
|
||||||
fieldset(class="preferences"):
|
fieldset(class="preferences"):
|
||||||
form(`method`="post", action="/saveprefs"):
|
form(`method`="post", action="/saveprefs", autocomplete="off"):
|
||||||
refererField path
|
refererField path
|
||||||
|
|
||||||
renderPrefs()
|
renderPrefs()
|
||||||
|
|
|
@ -23,7 +23,7 @@ const toggles = {
|
||||||
proc renderSearch*(): VNode =
|
proc renderSearch*(): VNode =
|
||||||
buildHtml(tdiv(class="panel-container")):
|
buildHtml(tdiv(class="panel-container")):
|
||||||
tdiv(class="search-bar"):
|
tdiv(class="search-bar"):
|
||||||
form(`method`="get", action="/search"):
|
form(`method`="get", action="/search", autocomplete="off"):
|
||||||
hiddenField("f", "users")
|
hiddenField("f", "users")
|
||||||
input(`type`="text", name="q", autofocus="", placeholder="ユーザー名の入力…", dir="auto")
|
input(`type`="text", name="q", autofocus="", placeholder="ユーザー名の入力…", dir="auto")
|
||||||
button(`type`="submit"): icon "search"
|
button(`type`="submit"): icon "search"
|
||||||
|
@ -57,7 +57,8 @@ proc isPanelOpen(q: Query): bool =
|
||||||
proc renderSearchPanel*(query: Query): VNode =
|
proc renderSearchPanel*(query: Query): VNode =
|
||||||
let user = query.fromUser.join(",")
|
let user = query.fromUser.join(",")
|
||||||
let action = if user.len > 0: &"/{user}/search" else: "/search"
|
let action = if user.len > 0: &"/{user}/search" else: "/search"
|
||||||
buildHtml(form(`method`="get", action=action, class="search-field")):
|
buildHtml(form(`method`="get", action=action,
|
||||||
|
class="search-field", autocomplete="off")):
|
||||||
hiddenField("f", "tweets")
|
hiddenField("f", "tweets")
|
||||||
genInput("q", "", query.text, "検索…", class="pref-inline")
|
genInput("q", "", query.text, "検索…", class="pref-inline")
|
||||||
button(`type`="submit"): icon "search"
|
button(`type`="submit"): icon "search"
|
||||||
|
@ -111,7 +112,7 @@ proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string;
|
||||||
proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode =
|
proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="timeline-container")):
|
buildHtml(tdiv(class="timeline-container")):
|
||||||
tdiv(class="timeline-header"):
|
tdiv(class="timeline-header"):
|
||||||
form(`method`="get", action="/search", class="search-field"):
|
form(`method`="get", action="/search", class="search-field", autocomplete="off"):
|
||||||
hiddenField("f", "users")
|
hiddenField("f", "users")
|
||||||
genInput("q", "", results.query.text, "ユーザー名の入力…", class="pref-inline")
|
genInput("q", "", results.query.text, "ユーザー名の入力…", class="pref-inline")
|
||||||
button(`type`="submit"): icon "search"
|
button(`type`="submit"): icon "search"
|
||||||
|
|
|
@ -95,7 +95,7 @@ proc renderTimelineTweets*(results: Result[Tweet]; prefs: Prefs; path: string;
|
||||||
if not results.beginning:
|
if not results.beginning:
|
||||||
renderNewer(results.query, parseUri(path).path)
|
renderNewer(results.query, parseUri(path).path)
|
||||||
|
|
||||||
if pinned.isSome:
|
if not prefs.hidePins and pinned.isSome:
|
||||||
let tweet = get pinned
|
let tweet = get pinned
|
||||||
renderTweet(tweet, prefs, path, showThread=tweet.hasThread)
|
renderTweet(tweet, prefs, path, showThread=tweet.hasThread)
|
||||||
|
|
||||||
|
|
新しいイシューから参照