diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index fde1b09..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,80 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the master branch - push: - branches: [ master ] - pull_request: - branches: [ master ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - test_stable: - runs-on: ubuntu-latest - strategy: - matrix: - firefox: [ '73.0' ] - include: - - nim-version: 'stable' - cache-key: 'stable' - steps: - - uses: actions/checkout@v2 - - name: Checkout submodules - run: git submodule update --init --recursive - - - name: Setup firefox - uses: browser-actions/setup-firefox@latest - with: - firefox-version: ${{ matrix.firefox }} - - - name: Get Date - id: get-date - run: echo "::set-output name=date::$(date "+%Y-%m-%d")" - shell: bash - - - name: Cache choosenim - uses: actions/cache@v2 - with: - path: ~/.choosenim - key: ${{ runner.os }}-choosenim-${{ matrix.cache-key }} - - - name: Cache nimble - uses: actions/cache@v2 - with: - path: ~/.nimble - key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} - - - uses: jiro4989/setup-nim-action@v1 - with: - nim-version: "${{ matrix.nim-version }}" - - - name: Install geckodriver - run: | - sudo apt-get -qq update - sudo apt-get install autoconf libtool libsass-dev - - wget https://github.com/mozilla/geckodriver/releases/download/v0.29.1/geckodriver-v0.29.1-linux64.tar.gz - mkdir geckodriver - tar -xzf geckodriver-v0.29.1-linux64.tar.gz -C geckodriver - export PATH=$PATH:$PWD/geckodriver - - - name: Install choosenim - run: | - export CHOOSENIM_CHOOSE_VERSION="stable" - curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh - sh init.sh -y - export PATH=$HOME/.nimble/bin:$PATH - nimble refresh -y - - - name: Run tests - run: | - export MOZ_HEADLESS=1 - nimble -y install - nimble -y test - diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b5c6c9b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,45 @@ +os: + - linux + +language: c + +cache: + directories: + - "$HOME/.nimble" + - "$HOME/.choosenim" + +addons: + firefox: "73.0" + +before_install: + - sudo apt-get -qq update + - sudo apt-get install autoconf libtool + - git clone -b 3.5.4 https://github.com/sass/libsass.git + - cd libsass + - autoreconf --force --install + - | + ./configure \ + --disable-tests \ + --disable-static \ + --enable-shared \ + --prefix=/usr + - sudo make -j5 install + - cd .. + + - wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz + - mkdir geckodriver + - tar -xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver + - export PATH=$PATH:$PWD/geckodriver + +install: + - export CHOOSENIM_CHOOSE_VERSION="stable" + - | + curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh + sh init.sh -y + - export PATH=$HOME/.nimble/bin:$PATH + - nimble refresh -y + +script: + - export MOZ_HEADLESS=1 + - nimble -y install + - nimble -y test diff --git a/README.md b/README.md index d7dedb4..05d4667 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ nimble frontend nimble backend ``` + Development typically involves running `nimble devdb` which sets up the database for development and testing, then `nimble backend` which compiles and runs the forum's backend, and `nimble frontend` @@ -87,38 +88,6 @@ separately to build the frontend. When making changes to the frontend it should be enough to simply run `nimble frontend` again to rebuild. This command will also build the SASS ``nimforum.scss`` file in the `public/css` directory. -### With docker - -You can easily launch site on localhost if you have `docker` and `docker-compose`. -You don't have to setup dependencies (libsass, sglite, pcre, etc...) on you host PC. - -To get up and running: - -```bash -cd docker -docker-compose build -docker-compose up -``` - -And you can access local NimForum site. -Open http://localhost:5000 . - -# Troubleshooting - -You might have to run `nimble install karax@#5f21dcd`, if setup fails -with: - -``` -andinus@circinus ~/projects/forks/nimforum> nimble --verbose devdb -[...] - Installing karax@#5f21dcd - Tip: 24 messages have been suppressed, use --verbose to show them. - Error: No binaries built, did you specify a valid binary name? -[...] - Error: Exception raised during nimble script execution -``` - -The hash needs to be replaced with the one specified in output. # Copyright diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index cb3191a..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM nimlang/nim:1.2.6-ubuntu - -RUN apt-get update -yqq \ - && apt-get install -y --no-install-recommends \ - libsass-dev \ - sqlite3 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY . /app - -# install dependencies -RUN nimble install -Y diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 8657235..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3.7" - -services: - forum: - build: - context: ../ - dockerfile: ./docker/Dockerfile - volumes: - - "../:/app" - ports: - - "5000:5000" - entrypoint: "/app/docker/entrypoint.sh" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index d8f5923..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -set -eu - -git submodule update --init --recursive - -# setup -nimble c -d:release src/setup_nimforum.nim -./src/setup_nimforum --dev - -# build frontend -nimble c -r src/buildcss -nimble js -d:release src/frontend/forum.nim -mkdir -p public/js -cp src/frontend/forum.js public/js/forum.js - -# build backend -nimble c src/forum.nim -./src/forum diff --git a/nimforum.nimble b/nimforum.nimble index 58a22f7..3d21367 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.1.0" +version = "2.0.2" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" @@ -13,13 +13,13 @@ skipExt = @["nim"] # Dependencies requires "nim >= 1.0.6" -requires "jester#405be2e" -requires "bcrypt#440c5676ff6" +requires "jester#d8a03aa" +requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha#d06488e" requires "sass#649e0701fa5c" -requires "karax#5f21dcd" +requires "karax#f6bda9a" requires "webdriver#429933a" diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 2daecdb..5630b06 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -22,7 +22,7 @@ table th { // Custom styles. // - Navigation bar. $navbar-height: 60px; -$default-category-color: #a3a3a3; +$default-category-color: #98c766; $logo-height: $navbar-height - 20px; .navbar-button { @@ -301,14 +301,10 @@ $threads-meta-color: #545d70; } } -.thread-replies, .thread-time, .views-text, .popular-text, .centered-header { +.thread-replies, .thread-time, .thread-users, .views-text, .centered-header { text-align: center; } -.thread-users { - text-align: left; -} - .thread-time { color: $threads-meta-color; diff --git a/src/email.nim b/src/email.nim index 580cc76..d26f4ad 100644 --- a/src/email.nim +++ b/src/email.nim @@ -44,18 +44,8 @@ proc sendMail( warn("Cannot send mail: no smtp from address configured (smtpFromAddr).") return - var client: AsyncSmtp - if mailer.config.smtpTls: - client = newAsyncSmtp(useSsl=false) - await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) - await client.startTls() - elif mailer.config.smtpSsl: - client = newAsyncSmtp(useSsl=true) - await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) - else: - client = newAsyncSmtp(useSsl=false) - await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) - + var client = newAsyncSmtp() + await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) if mailer.config.smtpUser.len > 0: await client.auth(mailer.config.smtpUser, mailer.config.smtpPassword) @@ -64,9 +54,6 @@ proc sendMail( var headers = otherHeaders headers.add(("From", mailer.config.smtpFromAddr)) - let dateHeader = now().utc().format("ddd, dd MMM yyyy hh:mm:ss") & " +0000" - headers.add(("Date", dateHeader)) - let encoded = createMessage(subject, message, toList, @[], headers) diff --git a/src/forum.nim b/src/forum.nim index c2682eb..70baf54 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -276,26 +276,25 @@ template createTFD() = new(c) init(c) c.req = request - if cookies(request).len > 0: + if request.cookies.len > 0: checkLoggedIn(c) #[ DB functions. TODO: Move to another module? ]# proc selectUser(userRow: seq[string], avatarSize: int=80): User = result = User( - id: userRow[0], - name: userRow[1], - avatarUrl: userRow[2].getGravatarUrl(avatarSize), - lastOnline: userRow[3].parseInt, - previousVisitAt: userRow[4].parseInt, - rank: parseEnum[Rank](userRow[5]), - isDeleted: userRow[6] == "1" + name: userRow[0], + avatarUrl: userRow[1].getGravatarUrl(avatarSize), + lastOnline: userRow[2].parseInt, + previousVisitAt: userRow[3].parseInt, + rank: parseEnum[Rank](userRow[4]), + isDeleted: userRow[5] == "1" ) # Don't give data about a deleted user. if result.isDeleted: result.name = "DeletedUser" - result.avatarUrl = getGravatarUrl(result.name & userRow[2], avatarSize) + result.avatarUrl = getGravatarUrl(result.name & userRow[1], avatarSize) proc selectPost(postRow: seq[string], skippedPosts: seq[int], replyingTo: Option[PostLink], history: seq[PostInfo], @@ -303,7 +302,7 @@ proc selectPost(postRow: seq[string], skippedPosts: seq[int], return Post( id: postRow[0].parseInt, replyingTo: replyingTo, - author: selectUser(postRow[5..11]), + author: selectUser(postRow[5..10]), likes: likes, seen: false, # TODO: history: history, @@ -319,7 +318,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = const replyingToQuery = sql""" select p.id, strftime('%s', p.creation), p.thread, - u.id, u.name, u.email, strftime('%s', u.lastOnline), + u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted, t.name @@ -335,7 +334,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = topic: row[^1], threadId: row[2].parseInt(), postId: row[0].parseInt(), - author: some(selectUser(row[3..9])) + author: some(selectUser(row[3..8])) )) proc selectHistory(postId: int): seq[PostInfo] = @@ -354,7 +353,7 @@ proc selectHistory(postId: int): seq[PostInfo] = proc selectLikes(postId: int): seq[User] = const likeQuery = sql""" - select u.id, u.name, u.email, strftime('%s', u.lastOnline), + select u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from like h, person u @@ -369,7 +368,7 @@ proc selectLikes(postId: int): seq[User] = proc selectThreadAuthor(threadId: int): User = const authorQuery = sql""" - select id, name, email, strftime('%s', lastOnline), + select name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, isDeleted from person where id in ( select author from post @@ -387,7 +386,7 @@ proc selectThread(threadRow: seq[string], author: User): Thread = where thread = ?;""" const usersListQuery = sql""" - select u.id, name, email, strftime('%s', lastOnline), + select name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, u.isDeleted, count(*) from person u, post p where p.author = u.id and p.thread = ? @@ -400,10 +399,10 @@ proc selectThread(threadRow: seq[string], author: User): Thread = id: threadRow[0].parseInt, topic: threadRow[1], category: Category( - id: threadRow[6].parseInt, - name: threadRow[7], - description: threadRow[8], - color: threadRow[9] + id: threadRow[5].parseInt, + name: threadRow[6], + description: threadRow[7], + color: threadRow[8] ), users: @[], replies: posts[0].parseInt-1, @@ -412,7 +411,6 @@ proc selectThread(threadRow: seq[string], author: User): Thread = creation: posts[1].parseInt, isLocked: threadRow[4] == "1", isSolved: false, # TODO: Add a field to `post` to identify the solution. - isPinned: threadRow[5] == "1" ) # Gather the users list. @@ -500,10 +498,10 @@ proc updatePost(c: TForumData, postId: int, content: string, # Verify that the current user has permissions to edit the specified post. let creation = fromUnix(postRow[1].parseInt) - let isArchived = (getTime() - creation).inHours >= 2 + let isArchived = (getTime() - creation).inWeeks > 8 let canEdit = c.rank == Admin or c.userid == postRow[0] - if isArchived and c.rank < Admin: - raise newForumError("This post is too old and can no longer be edited") + if isArchived: + raise newForumError("This post is archived and can no longer be edited") if not canEdit: raise newForumError("You cannot edit this post") @@ -534,7 +532,7 @@ proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], query let threadAuthor = selectThreadAuthor(threadId.parseInt) # Verify that the current user has permissions to edit the specified thread. - let canEdit = c.rank in {Admin, Moderator} or c.userid == threadAuthor.id + let canEdit = c.rank == Admin or c.userid == threadAuthor.name if not canEdit: raise newForumError("You cannot edit this thread") @@ -710,25 +708,15 @@ proc executeLockState(c: TForumData, threadId: int, locked: bool) = # Save the like. exec(db, crud(crUpdate, "thread", "isLocked"), locked.int, threadId) -proc executePinState(c: TForumData, threadId: int, pinned: bool) = - if c.rank < Moderator: - raise newForumError("You do not have permission to pin this thread.") - - # (Un)pin this thread - exec(db, crud(crUpdate, "thread", "isPinned"), pinned.int, threadId) - proc executeDeletePost(c: TForumData, postId: int) = # Verify that this post belongs to the user. const postQuery = sql""" - select p.author, p.id from post p + select p.id from post p where p.author = ? and p.id = ? """ - let - row = getRow(db, postQuery, c.username, postId) - author = row[0] - id = row[1] + let id = getValue(db, postQuery, c.username, postId) - if id.len == 0 and not (c.rank == Admin or c.userid == author): + if id.len == 0 and c.rank < Admin: raise newForumError("You cannot delete this post") # Set the `isDeleted` flag. @@ -783,7 +771,7 @@ proc updateProfile( raise newForumError("Rank needs a change when setting new email.") await sendSecureEmail( - mailer, ActivateEmail, c.req, row[0], row[1], email, row[3] + mailer, ActivateEmail, c.req, row[0], row[1], row[2], row[3] ) validateEmail(email, checkDuplicated=wasEmailChanged) @@ -841,27 +829,27 @@ routes: categoryArgs.insert($categoryId, 0) const threadsQuery = - """select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned, + """select t.id, t.name, views, strftime('%s', modified), isLocked, c.id, c.name, c.description, c.color, - u.id, u.name, u.email, strftime('%s', u.lastOnline), + u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from thread t, category c, person u where t.isDeleted = 0 and category = c.id and $# u.status <> 'Spammer' and u.status <> 'Troll' and - u.id = ( - select p.author from post p - where p.thread = t.id - order by p.author + u.id in ( + select u.id from post p, person u + where p.author = u.id and p.thread = t.id + order by u.id limit 1 ) - order by isPinned desc, modified desc limit ?, ?;""" + order by modified desc limit ?, ?;""" let thrCount = getValue(db, countQuery, countArgs).parseInt() let moreCount = max(0, thrCount - (start + count)) var list = ThreadList(threads: @[], moreCount: moreCount) for data in getAllRows(db, sql(threadsQuery % categorySection), categoryArgs): - let thread = selectThread(data[0 .. 9], selectUser(data[10 .. ^1])) + let thread = selectThread(data[0 .. 8], selectUser(data[9 .. ^1])) list.threads.add(thread) resp $(%list), "application/json" @@ -876,24 +864,19 @@ routes: count = 10 const threadsQuery = - sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned, + sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, c.id, c.name, c.description, c.color from thread t, category c where t.id = ? and isDeleted = 0 and category = c.id;""" let threadRow = getRow(db, threadsQuery, id) - if threadRow[0].len == 0: - let err = PostError( - message: "Specified thread does not exist" - ) - resp Http404, $(%err), "application/json" let thread = selectThread(threadRow, selectThreadAuthor(id)) let postsQuery = sql( """select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - u.id, u.name, u.email, strftime('%s', u.lastOnline), + u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from post p, person u @@ -932,20 +915,15 @@ routes: get "/specific_posts.json": createTFD() - var ids: JsonNode - try: + var ids = parseJson(@"ids") - except JsonParsingError: - let err = PostError( - message: "Invalid JSON in the `ids` parameter" - ) - resp Http400, $(%err), "application/json" + cond ids.kind == JArray let intIDs = ids.elems.map(x => x.getInt()) let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - u.id, u.name, u.email, strftime('%s', u.lastOnline), + u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from post p, person u @@ -1014,7 +992,7 @@ routes: """ % postsFrom) let userQuery = sql(""" - select id, name, email, strftime('%s', lastOnline), + select name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, isDeleted, strftime('%s', creation), id from person @@ -1040,7 +1018,7 @@ routes: getValue(db, sql("select count(*) " & threadsFrom), userID).parseInt() if c.rank >= Admin or c.username == username: - profile.email = some(userRow[2]) + profile.email = some(userRow[1]) for row in db.getAllRows(postsQuery, username): profile.posts.add( @@ -1357,33 +1335,6 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" - post re"/(pin|unpin)": - createTFD() - if not c.loggedIn(): - let err = PostError( - errorFields: @[], - message: "Not logged in." - ) - resp Http401, $(%err), "application/json" - - let formData = request.formData - cond "id" in formData - - let threadId = getInt(formData["id"].body, -1) - cond threadId != -1 - - try: - case request.path - of "/pin": - executePinState(c, threadId, true) - of "/unpin": - executePinState(c, threadId, false) - else: - assert false - resp Http200, "{}", "application/json" - except ForumError as exc: - resp Http400, $(%exc.data), "application/json" - post re"/delete(Post|Thread)": createTFD() if not c.loggedIn(): @@ -1616,7 +1567,7 @@ routes: postId: rowFT[2].parseInt(), postContent: content, creation: rowFT[4].parseInt(), - author: selectUser(rowFT[5 .. 11]), + author: selectUser(rowFT[5 .. 10]), ) ) diff --git a/src/frontend/header.nim b/src/frontend/header.nim index 7cfb133..cde48de 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -96,8 +96,8 @@ when defined(js): section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="search-input input-sm", - `type`="search", placeholder="Search", - id="search-box", required="required", + `type`="text", placeholder="search", + id="search-box", onKeyDown=onKeyDown) if state.loading: tdiv(class="loading") diff --git a/src/frontend/post.nim b/src/frontend/post.nim index dc12e47..0530814 100644 --- a/src/frontend/post.nim +++ b/src/frontend/post.nim @@ -64,4 +64,4 @@ when defined(js): renderPostUrl(thread.id, post.id) proc renderPostUrl*(link: PostLink): string = - renderPostUrl(link.threadId, link.postId) + renderPostUrl(link.threadId, link.postId) \ No newline at end of file diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index 9fa4ab4..8bd9c34 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -190,7 +190,7 @@ when defined(js): else: "" result = buildHtml(): - button(class="btn btn-secondary", id="lock-btn", + button(class="btn btn-secondary", onClick=(e: Event, n: VNode) => onLockClick(e, n, state, thread), "data-tooltip"=tooltip, @@ -201,61 +201,4 @@ when defined(js): text " Unlock Thread" else: italic(class="fas fa-lock") - text " Lock Thread" - - type - PinButton* = ref object - error: Option[PostError] - loading: bool - - proc newPinButton*(): PinButton = - PinButton() - - proc onPost(httpStatus: int, response: kstring, state: PinButton, - thread: var Thread) = - postFinished: - thread.isPinned = not thread.isPinned - - proc onPinClick(ev: Event, n: VNode, state: PinButton, thread: var Thread) = - if state.loading: return - - state.loading = true - state.error = none[PostError]() - - # Same as LockButton so the following is still a hack and karax should support this. - var formData = newFormData() - formData.append("id", $thread.id) - let uri = - if thread.isPinned: - makeUri("/unpin") - else: - makeUri("/pin") - ajaxPost(uri, @[], formData.to(cstring), - (s: int, r: kstring) => onPost(s, r, state, thread)) - - ev.preventDefault() - - proc render*(state: PinButton, thread: var Thread, - currentUser: Option[User]): VNode = - if currentUser.isNone() or - currentUser.get().rank < Moderator: - return buildHtml(tdiv()) - - let tooltip = - if state.error.isSome(): state.error.get().message - else: "" - - result = buildHtml(): - button(class="btn btn-secondary", id="pin-btn", - onClick=(e: Event, n: VNode) => - onPinClick(e, n, state, thread), - "data-tooltip"=tooltip, - onmouseleave=(e: Event, n: VNode) => - (state.error = none[PostError]())): - if thread.isPinned: - italic(class="fas fa-thumbtack") - text " Unpin Thread" - else: - italic(class="fas fa-thumbtack") - text " Pin Thread" - + text " Lock Thread" \ No newline at end of file diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 66b3162..305e059 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -36,7 +36,6 @@ when defined(js): likeButton: LikeButton deleteModal: DeleteModal lockButton: LockButton - pinButton: PinButton categoryPicker: CategoryPicker proc onReplyPosted(id: int) @@ -57,7 +56,6 @@ when defined(js): likeButton: newLikeButton(), deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), lockButton: newLockButton(), - pinButton: newPinButton(), categoryPicker: newCategoryPicker(onCategoryChanged) ) @@ -211,12 +209,12 @@ when defined(js): let loggedIn = currentUser.isSome() let authoredByUser = loggedIn and currentUser.get().name == thread.author.name - let canChangeCategory = - loggedIn and currentUser.get().rank in {Admin, Moderator} + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin result = buildHtml(): tdiv(): - if authoredByUser or canChangeCategory: + if authoredByUser or currentAdmin: render(state.categoryPicker, currentUser, compact=false) else: render(thread.category) @@ -413,7 +411,6 @@ when defined(js): text " Reply" render(state.lockButton, list.thread, currentUser) - render(state.pinButton, list.thread, currentUser) render(state.replyBox, list.thread, state.replyingTo, false) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index ecec6da..d2dc104 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -15,7 +15,6 @@ type creation*: int64 ## Unix timestamp isLocked*: bool isSolved*: bool - isPinned*: bool ThreadList* = ref object threads*: seq[Thread] @@ -97,18 +96,15 @@ when defined(js): else: return $duration.inSeconds & "s" - proc genThread(pos: int, thread: Thread, isNew: bool, noBorder: bool, displayCategory=true): VNode = + proc genThread(thread: Thread, isNew: bool, noBorder: bool, displayCategory=true): VNode = let isOld = (getTime() - thread.creation.fromUnix).inWeeks > 2 let isBanned = thread.author.rank.isBanned() result = buildHtml(): - tr(class=class({"no-border": noBorder, "banned": isBanned, "pinned": thread.isPinned, "thread-" & $pos: true})): + tr(class=class({"no-border": noBorder, "banned": isBanned})): td(class="thread-title"): if thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") - if thread.isPinned: - italic(class="fas fa-thumbtack fa-xs", - title="Pinned post") if isBanned: italic(class="fas fa-ban fa-xs", title="Thread author is banned") @@ -120,11 +116,8 @@ when defined(js): title="Thread has a solution") a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic - tdiv(class="show-sm" & class({"d-none": not displayCategory})): + tdiv(class=class({"d-none": not displayCategory})): render(thread.category) - - td(class="hide-sm" & class({"d-none": not displayCategory})): - render(thread.category) genUserAvatars(thread.users) td(class="thread-replies"): text $thread.replies td(class="hide-sm" & class({ @@ -206,7 +199,7 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) - let displayCategory = categoryId.isNone + let displayCategory = true let list = state.list.get() result = buildHtml(): @@ -215,8 +208,7 @@ when defined(js): thead(): tr: th(text "Topic") - th(class="hide-sm" & class({"d-none": not displayCategory})): text "Category" - th(class="thread-users"): text "Users" + th(class="centered-header"): text "Users" th(class="centered-header"): text "Replies" th(class="hide-sm centered-header"): text "Views" th(class="centered-header"): text "Activity" @@ -227,7 +219,7 @@ when defined(js): let isLastThread = i+1 == list.threads.len let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser) - genThread(i+1, thread, isNew, + genThread(thread, isNew, noBorder=isLastUnseen or isLastThread, displayCategory=displayCategory) if isLastUnseen and (not isLastThread): diff --git a/src/frontend/user.nim b/src/frontend/user.nim index db874c3..fe4efa7 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -17,7 +17,6 @@ type Admin ## Admin: can do everything User* = object - id*: string name*: string avatarUrl*: string lastOnline*: int64 @@ -76,4 +75,4 @@ when defined(js): title="User is a moderator") of Admin: italic(class="fas fa-chess-knight", - title="User is an admin") + title="User is an admin") \ No newline at end of file diff --git a/src/fts.sql b/src/fts.sql index 1590a05..e5490f2 100644 --- a/src/fts.sql +++ b/src/fts.sql @@ -7,7 +7,6 @@ SELECT post_id, post_content, cdate, - person.id, person.name AS author, person.email AS email, strftime('%s', person.lastOnline) AS lastOnline, @@ -47,7 +46,6 @@ SELECT THEN snippet(post_fts, '**', '**', '...', what, -45) ELSE SUBSTR(post_fts.content, 1, 200) END AS content, cdate, - person.id, person.name AS author, person.email AS email, strftime('%s', person.lastOnline) AS lastOnline, diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index c80ad3b..b235415 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -66,7 +66,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec(sql""" insert into category (id, name, description, color) - values (0, 'Unsorted', 'No category has been chosen yet.', ''); + values (0, 'Default', 'The default category', ''); """) # -- Thread @@ -81,7 +81,6 @@ proc initialiseDb(admin: tuple[username, password, email: string], isLocked boolean not null default 0, solution integer, isDeleted boolean not null default 0, - isPinned boolean not null default 0, foreign key (category) references category(id), foreign key (solution) references post(id) @@ -235,7 +234,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], proc initialiseConfig( name, title, hostname: string, recaptcha: tuple[siteKey, secretKey: string], - smtp: tuple[address, user, password, fromAddr: string, tls: bool], + smtp: tuple[address, user, password, fromAddr: string], isDev: bool, dbPath: string, ga: string="" @@ -252,7 +251,6 @@ proc initialiseConfig( "smtpUser": %smtp.user, "smtpPassword": %smtp.password, "smtpFromAddr": %smtp.fromAddr, - "smtpTls": %smtp.tls, "isDev": %isDev, "dbPath": %dbPath } @@ -284,7 +282,7 @@ These can be changed later in the generated forum.json file. echo("") echo("The following question are related to recaptcha. \nYou must set up a " & - "recaptcha v2 for your forum before answering them. \nPlease do so now " & + "recaptcha for your forum before answering them. \nPlease do so now " & "and then answer these questions: https://www.google.com/recaptcha/admin") let recaptchaSiteKey = question("Recaptcha site key: ") let recaptchaSecretKey = question("Recaptcha secret key: ") @@ -296,7 +294,6 @@ These can be changed later in the generated forum.json file. let smtpUser = question("SMTP user: ") let smtpPassword = readPasswordFromStdin("SMTP pass: ") let smtpFromAddr = question("SMTP sending email address (eg: mail@mail.hostname.com): ") - let smtpTls = parseBool(question("Enable TLS for SMTP: ")) echo("The following is optional. You can specify your Google Analytics ID " & "if you wish. Otherwise just leave it blank.") @@ -306,7 +303,7 @@ These can be changed later in the generated forum.json file. let dbPath = "nimforum.db" initialiseConfig( name, title, hostname, (recaptchaSiteKey, recaptchaSecretKey), - (smtpAddress, smtpUser, smtpPassword, smtpFromAddr, smtpTls), isDev=false, + (smtpAddress, smtpUser, smtpPassword, smtpFromAddr), isDev=false, dbPath, ga ) @@ -341,7 +338,7 @@ when isMainModule: "Development Forum", "localhost", recaptcha=("", ""), - smtp=("", "", "", "", false), + smtp=("", "", "", ""), isDev=true, dbPath ) @@ -358,7 +355,7 @@ when isMainModule: "Test Forum", "localhost", recaptcha=("", ""), - smtp=("", "", "", "", false), + smtp=("", "", "", ""), isDev=true, dbPath ) diff --git a/src/utils.nim b/src/utils.nim index 4b1d339..1be058b 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,11 +1,11 @@ import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, htmlparser, streams, parseutils, options, logging -from times import getTime, utc, format +from times import getTime, getGMTime, format # Used to be: # {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} let - UsernameIdent* = IdentChars + {'-'} # TODO: Double check that everyone follows this. + UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. import frontend/[karaxutils, error] export parseInt @@ -17,8 +17,6 @@ type smtpUser*: string smtpPassword*: string smtpFromAddr*: string - smtpTls*: bool - smtpSsl*: bool mlistAddress*: string recaptchaSecretKey*: string recaptchaSiteKey*: string @@ -57,8 +55,6 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpUser = root{"smtpUser"}.getStr("") result.smtpPassword = root{"smtpPassword"}.getStr("") result.smtpFromAddr = root{"smtpFromAddr"}.getStr("") - result.smtpTls = root{"smtpTls"}.getBool(false) - result.smtpSsl = root{"smtpSsl"}.getBool(false) result.mlistAddress = root{"mlistAddress"}.getStr("") result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 8c27a87..b1a2edb 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -69,29 +69,6 @@ proc categoriesUserTests(session: Session, baseUrl: string) = ensureExists title, LinkTextSelector - test "can create category thread and change category": - with session: - let newTitle = title & " Selection" - click "#new-thread-btn" - sendKeys "#thread-title", newTitle - - selectCategory "fun" - sendKeys "#reply-textarea", content - - click "#create-thread-btn" - checkText "#thread-title .category", "Fun" - - selectCategory "announcements" - - checkText "#thread-title .category", "Announcements" - - # Make sure there is no error - checkIsNone "#thread-title .text-error" - - navigate baseUrl - - ensureExists newTitle, LinkTextSelector - test "can navigate to categories page": with session: click "#categories-btn" @@ -125,7 +102,7 @@ proc categoriesUserTests(session: Session, baseUrl: string) = click "#new-thread-btn" sendKeys "#thread-title", "Post 3" - selectCategory "unsorted" + selectCategory "default" sendKeys "#reply-textarea", "Post 3" click "#create-thread-btn" @@ -135,11 +112,11 @@ proc categoriesUserTests(session: Session, baseUrl: string) = click "#categories-btn" ensureExists "#categories-list" - click "#category-unsorted" + click "#category-default" checkText "#threads-list .thread-title a", "Post 3" for element in session.waitForElements("#threads-list .category-name"): # Have to user "innerText" because elements are hidden on this page - assert element.getProperty("innerText") == "Unsorted" + assert element.getProperty("innerText") == "Default" selectCategory "announcements" checkText "#threads-list .thread-title a", "Post 2" diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index e5924f3..d906675 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -30,8 +30,7 @@ proc elementIsSome(element: Option[Element]): bool = proc elementIsNone(element: Option[Element]): bool = return element.isNone -proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, - waitCondition: proc(element: Option[Element]): bool = elementIsSome): Option[Element] +proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, waitCondition=elementIsSome): Option[Element] proc click*(session: Session, element: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) @@ -72,14 +71,14 @@ proc setColor*(session: Session, element, color: string, strategy=CssSelector) = proc checkIsNone*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy, waitCondition=elementIsNone) -template checkText*(session: Session, element, expectedValue: string) = +proc checkText*(session: Session, element, expectedValue: string) = let el = session.waitForElement(element) check el.get().getText() == expectedValue proc waitForElement*( session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, - waitCondition: proc(element: Option[Element]): bool = elementIsSome + waitCondition=elementIsSome ): Option[Element] = var waitTime = 0 diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index dc78007..6e10e4c 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -40,4 +40,4 @@ proc test*(session: Session, baseUrl: string) = register "TEst1", "test1", verify = false ensureExists "#signup-form .has-error" - navigate baseUrl + navigate baseUrl \ No newline at end of file diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 32ce686..b79d93a 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,4 +1,5 @@ import unittest, common + import webdriver let @@ -39,53 +40,10 @@ proc userTests(session: Session, baseUrl: string) = checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr - test "can delete thread": - with session: - # create thread to be deleted - click "#new-thread-btn" - - sendKeys "#thread-title", "To be deleted" - sendKeys "#reply-textarea", "This thread is to be deleted" - - click "#create-thread-btn" - - click ".post-buttons .delete-button" - - # click delete confirmation - click "#delete-modal .delete-btn" - - # Make sure the forum post is gone - checkIsNone "To be deleted", LinkTextSelector - - test "cannot (un)pin thread": - with session: - navigate(baseUrl) - - click "#new-thread-btn" - - sendKeys "#thread-title", "Unpinnable" - sendKeys "#reply-textarea", "Cannot (un)pin as an user" - - click "#create-thread-btn" - - checkIsNone "#pin-btn" - - test "cannot lock threads": - with session: - navigate(baseUrl) - - click "#new-thread-btn" - - sendKeys "#thread-title", "Locking" - sendkeys "#reply-textarea", "Cannot lock as an user" - - click "#create-thread-btn" - - checkIsNone "#lock-btn" - session.logout() proc anonymousTests(session: Session, baseUrl: string) = + suite "anonymous user tests": with session: navigate baseUrl @@ -185,70 +143,6 @@ proc adminTests(session: Session, baseUrl: string) = # Make sure the forum post is gone checkIsNone adminTitleStr, LinkTextSelector - test "can pin a thread": - with session: - click "#new-thread-btn" - sendKeys "#thread-title", "Pinned post" - sendKeys "#reply-textarea", "A pinned post" - click "#create-thread-btn" - - navigate(baseUrl) - click "#new-thread-btn" - sendKeys "#thread-title", "Normal post" - sendKeys "#reply-textarea", "A normal post" - click "#create-thread-btn" - - navigate(baseUrl) - click "Pinned post", LinkTextSelector - click "#pin-btn" - checkText "#pin-btn", "Unpin Thread" - - navigate(baseUrl) - - # Make sure pin exists - ensureExists "#threads-list .thread-1 .thread-title i" - - checkText "#threads-list .thread-1 .thread-title a", "Pinned post" - checkText "#threads-list .thread-2 .thread-title a", "Normal post" - - test "can unpin a thread": - with session: - click "Pinned post", LinkTextSelector - click "#pin-btn" - checkText "#pin-btn", "Pin Thread" - - navigate(baseUrl) - - checkIsNone "#threads-list .thread-2 .thread-title i" - - checkText "#threads-list .thread-1 .thread-title a", "Normal post" - checkText "#threads-list .thread-2 .thread-title a", "Pinned post" - - test "can lock a thread": - with session: - click "Locking", LinkTextSelector - click "#lock-btn" - - ensureExists "#thread-title i.fas.fa-lock.fa-xs" - - test "locked thread appears on frontpage": - with session: - click "#new-thread-btn" - sendKeys "#thread-title", "A new locked thread" - sendKeys "#reply-textarea", "This thread should appear locked on the frontpage" - click "#create-thread-btn" - click "#lock-btn" - - navigate(baseUrl) - ensureExists "#threads-list .thread-1 .thread-title i.fas.fa-lock.fa-xs" - - test "can unlock a thread": - with session: - click "Locking", LinkTextSelector - click "#lock-btn" - - checkIsNone "#thread-title i.fas.fa-lock.fa-xs" - session.logout() proc test*(session: Session, baseUrl: string) = @@ -264,4 +158,4 @@ proc test*(session: Session, baseUrl: string) = unBanUser(session, baseUrl) - session.navigate(baseUrl) + session.navigate(baseUrl) \ No newline at end of file