Compare commits

...

No commits in common. "8b4dc49cf18e17021777a601bc39bb446b1926e6" and "2dee05c7f3673e939157eefd8568fdfae087f702" have entirely different histories.

260 changed files with 644843 additions and 49 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.go linguist-detectable=true
*.js linguist-detectable=false

12
.github/semantic.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# Always validate the PR title AND all the commits
titleAndCommits: true
# Require at least one commit to be valid
# this is only relevant when using commitsOnly: true or titleAndCommits: true,
# which validate all commits by default
anyCommit: true
# Allow use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns")
# this is only relevant when using commitsOnly: true (or titleAndCommits: true)
allowMergeCommits: false
# Allow use of Revert commits (eg on github: "Revert "feat: ride unicorns"")
# this is only relevant when using commitsOnly: true (or titleAndCommits: true)
allowRevertCommits: false

109
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,109 @@
name: Build
on: [push, pull_request]
jobs:
frontend:
name: Front-end
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- uses: c-hive/gha-yarn-cache@v2
with:
directory: ./web
- run: yarn install && CI=false yarn run build
working-directory: ./web
backend:
name: Back-end
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.16.5'
- name: Build
run: |
go build -race -ldflags "-extldflags '-static'"
working-directory: ./
linter:
name: Go-Linter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '^1.16.5'
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.29
args: --disable-all -E gofumpt --max-same-issues=0 --max-issues-per-linter=0 --timeout 5m ./...
release-and-push:
name: Release And Push
runs-on: ubuntu-latest
if: github.repository == 'casbin/casnode' && github.event_name == 'push'
needs: [ frontend, backend, linter ]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Fetch Previous version
id: get-previous-tag
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
- name: Release
run: yarn global add semantic-release@17.4.4 && semantic-release
env:
GH_TOKEN: ${{ secrets.GH_BOT_TOKEN }}
- name: Fetch Current version
id: get-current-tag
uses: actions-ecosystem/action-get-latest-tag@v1.6.0
- name: Decide Should_Push Or Not
id: should_push
run: |
old_version=${{steps.get-previous-tag.outputs.tag}}
new_version=${{steps.get-current-tag.outputs.tag }}
old_array=(${old_version//\./ })
new_array=(${new_version//\./ })
if [ ${old_array[0]} != ${new_array[0]} ]
then
echo "push='true'" >> GITHUB_OUTPUT
elif [ ${old_array[1]} != ${new_array[1]} ]
then
echo "push='true'" >> GITHUB_OUTPUT
else
echo "push='false'" >> GITHUB_OUTPUT
fi
- name: Log in to Docker Hub
uses: docker/login-action@v1
if: github.repository == 'casbin/casnode' && github.event_name == 'push' &&steps.should_push.outputs.push=='true'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Push to Docker Hub
uses: docker/build-push-action@v2
if: github.repository == 'casbin/casnode' && github.event_name == 'push' && steps.should_push.outputs.push=='true'
with:
push: true
tags: casbin/casnode:${{steps.get-current-tag.outputs.tag }},casbin/casnode:latest

35
.github/workflows/sync.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Crowdin Action
on:
push:
branches: [ master ]
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: crowdin action
uses: crowdin/github-action@1.4.8
with:
upload_translations: true
download_translations: true
push_translations: true
commit_message: 'refactor: New Crowdin translations by Github Action'
localization_branch_name: l10n_crowdin_action
create_pull_request: true
pull_request_title: 'refactor: New Crowdin translations'
crowdin_branch_name: l10n_branch
config: './web/crowdin.yml'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: '479711'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

14
.gitignore vendored
View File

@ -1,7 +1,3 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
@ -18,6 +14,12 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
.idea/
*.iml
tmp/
tmpFiles/
*.tmp
logs/
lastupdate.tmp
commentsRouter*.go

23
.releaserc.json Normal file
View File

@ -0,0 +1,23 @@
{
"debug": true,
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"master",
{
"name": "rc"
},
{
"name": "beta",
"prerelease": true
},
{
"name": "alpha",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}

22
Dockerfile Normal file
View File

@ -0,0 +1,22 @@
FROM golang:1.17 AS BACK
WORKDIR /go/src/casnode
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOPROXY=https://goproxy.cn,direct go build -ldflags="-w -s" -o server . \
&& apt update && apt install wait-for-it && chmod +x /usr/bin/wait-for-it
FROM node:14.17.6 AS FRONT
WORKDIR /web
COPY ./web .
RUN yarn config set registry https://registry.npmmirror.com
RUN yarn install && yarn run build
FROM alpine:latest
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add curl
LABEL MAINTAINER="https://casnode.org/"
COPY --from=BACK /go/src/casnode/ ./
COPY --from=BACK /usr/bin/wait-for-it ./
RUN mkdir -p web/build && apk add --no-cache bash coreutils
COPY --from=FRONT /web/build /web/build
CMD ./wait-for-it db:3306 -- ./server

210
LICENSE
View File

@ -1,73 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

112
README.md
View File

@ -1,2 +1,110 @@
# caswaf
<h1 align="center" style="border-bottom: none;">📦⚡️ Casnode</h1>
<h3 align="center">An open-source forum (BBS) software developed by Go and React.</h3>
<p align="center">
<a href="#badge">
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
</a>
<a href="https://hub.docker.com/r/casbin/casnode">
<img alt="docker pull casbin/casnode" src="https://img.shields.io/docker/pulls/casbin/casnode.svg">
</a>
<a href="https://github.com/casbin/casnode/actions/workflows/build.yml">
<img alt="GitHub Workflow Status (branch)" src="https://github.com/casbin/jcasbin/workflows/build/badge.svg?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/releases/latest">
<img alt="GitHub Release" src="https://img.shields.io/github/v/release/casbin/casnode.svg">
</a>
<a href="https://hub.docker.com/repository/docker/casbin/casnode">
<img alt="Docker Image Version (latest semver)" src="https://img.shields.io/badge/Docker%20Hub-latest-brightgreen">
</a>
</p>
<p align="center">
<a href="https://goreportcard.com/report/github.com/casbin/casnode">
<img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/casbin/casnode?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/casbin/casnode?style=flat-square" alt="license">
</a>
<a href="https://github.com/casbin/casnode/issues">
<img alt="GitHub issues" src="https://img.shields.io/github/issues/casbin/casnode?style=flat-square">
</a>
<a href="#">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/casbin/casnode?style=flat-square">
</a>
<a href="https://github.com/casbin/casnode/network">
<img alt="GitHub forks" src="https://img.shields.io/github/forks/casbin/casnode?style=flat-square">
</a>
<a href="https://crowdin.com/project/casnode">
<img alt="Crowdin" src="https://badges.crowdin.net/casnode/localized.svg">
</a>
</p>
## Online demo
Deployed site: https://forum.casbin.com/
## Architecture
Casnode contains 2 parts:
Name | Description | Language | Source code
----|------|----|----
Frontend | Web frontend UI for Casnode | Javascript + React | https://github.com/casbin/casnode/tree/master/web
Backend | RESTful API backend for Casnode | Golang + Beego + MySQL | https://github.com/casbin/casnode
## Installation
Casnode uses Casdoor to manage members. So you need to create an organization and an application for Casnode in a Casdoor instance.
### Necessary configuration
#### Get the code
```shell
go get github.com/casbin/casnode
go get github.com/casdoor/casdoor
```
or
```shell
git clone https://github.com/casbin/casnode
git clone https://github.com/casdoor/casdoor
```
#### Setup database
Casnode will store its users, nodes and topics informations in a MySQL database named: `casnode`, will create it if not existed. The DB connection string can be specified at: https://github.com/casbin/casnode/blob/master/conf/app.conf
```ini
dataSourceName = root:123@tcp(localhost:3306)/
```
Casnode uses XORM to connect to DB, so all DBs supported by XORM can also be used.
#### Run casnode
- Configure and run casnode by yourself. If you want to learn more about casnode, you see [casnode installation](https://casnode.org/docs/installation).
- Install casnode using docker. you see [installation by docker](https://casnode.org/docs/Docker).
- Install casnode using BTpanel. you see [installation by BTpanel](https://casnode.org/docs/BTpanel).
- Open browser:
http://localhost:3000/
### Optional configuration
#### Setup your forum to enable some third-party login platform
Casnode uses Casdoor to manage members. If you want to log in with oauth, you should see [casdoor oauth configuration](https://casdoor.org/docs/provider/OAuth).
#### OSS, Mail, and SMS services
Casnode uses Casdoor to upload files to cloud storage, send Emails and send SMSs. See Casdoor for more details.
#### Github corner
We added a Github icon in the upper right corner, linking to your Github repository address.
You could set `ShowGithubCorner` to hidden it.
Configuration:
```javascript
export const ShowGithubCorner = true
export const GithubRepo = "https://github.com/casbin/casnode" //your github repository
```

44
Tag_test.go Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"testing"
"github.com/astaxie/beego"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
)
var adapter *object.Adapter
func TestTopicTag(t *testing.T) {
topics := []*object.Topic{}
adapter = object.NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), beego.AppConfig.String("dbName"))
err := adapter.Engine.Table("topic").Find(&topics)
if err != nil {
panic(err)
}
for _, topic := range topics {
if len(topic.Tags) == 0 || topic.Tags == nil {
topic.Tags = service.Finalword(topic.Content)
_, err := adapter.Engine.Id(topic.Id).Update(topic)
if err != nil {
panic(err)
}
}
}
}

91
casdoor/adapter.go Normal file
View File

@ -0,0 +1,91 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package casdoor
import (
"runtime"
"github.com/astaxie/beego"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
var (
adapter *Adapter = nil
CasdoorOrganization string
)
type Session struct {
SessionKey string `xorm:"char(64) notnull pk"`
SessionData []uint8 `xorm:"blob"`
SessionExpiry int `xorm:"notnull"`
}
func InitCasdoorAdapter() {
casdoorDbName := beego.AppConfig.String("casdoorDbName")
if casdoorDbName == "" {
return
}
adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), beego.AppConfig.String("casdoorDbName"))
CasdoorOrganization = beego.AppConfig.String("casdoorOrganization")
}
// Adapter represents the MySQL adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
dbName string
Engine *xorm.Engine
}
// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
err := a.Engine.Close()
if err != nil {
panic(err)
}
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
a.dbName = dbName
// Open the DB, create it if not existed.
a.open()
// Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer)
return a
}
func (a *Adapter) open() {
Engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName)
if err != nil {
panic(err)
}
a.Engine = Engine
}
func (a *Adapter) close() {
a.Engine.Close()
a.Engine = nil
}

82
casdoor/user.go Normal file
View File

@ -0,0 +1,82 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package casdoor
import "github.com/casdoor/casdoor-go-sdk/casdoorsdk"
func GetUsers() []*casdoorsdk.User {
if adapter != nil {
return getUsers()
} else {
users, err := casdoorsdk.GetUsers()
if err != nil {
panic(err)
}
return users
}
}
func GetSortedUsers(sorter string, limit int) []*casdoorsdk.User {
if adapter != nil {
return getSortedUsers(sorter, limit)
} else {
users, err := casdoorsdk.GetSortedUsers(sorter, limit)
if err != nil {
panic(err)
}
return users
}
}
func GetUserCount() int {
if adapter != nil {
return getUserCount()
} else {
count, err := casdoorsdk.GetUserCount("")
if err != nil {
panic(err)
}
return count
}
}
func GetOnlineUserCount() int {
if adapter != nil {
return getOnlineUserCount()
} else {
count, err := casdoorsdk.GetUserCount("1")
if err != nil {
panic(err)
}
return count
}
}
func GetUserByEmail(email string) *casdoorsdk.User {
if adapter != nil {
return getUserByEmail(email)
} else {
user, err := casdoorsdk.GetUserByEmail(email)
if err != nil {
panic(err)
}
return user
}
}

220
casdoor/user_adapter.go Normal file
View File

@ -0,0 +1,220 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package casdoor
import (
"fmt"
"time"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"xorm.io/core"
)
func getUsers() []*casdoorsdk.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
users := []*casdoorsdk.User{}
err := adapter.Engine.Desc("created_time").Find(&users, &casdoorsdk.User{Owner: owner})
if err != nil {
panic(err)
}
return users
}
func getSortedUsers(sorter string, limit int) []*casdoorsdk.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
users := []*casdoorsdk.User{}
err := adapter.Engine.Desc(sorter).Limit(limit, 0).Find(&users, &casdoorsdk.User{Owner: owner})
if err != nil {
panic(err)
}
return users
}
func getUserCount() int {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
count, err := adapter.Engine.Count(&casdoorsdk.User{Owner: owner})
if err != nil {
panic(err)
}
return int(count)
}
func getOnlineUserCount() int {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
count, err := adapter.Engine.Where("is_online = ?", 1).Count(&casdoorsdk.User{Owner: owner})
if err != nil {
panic(err)
}
return int(count)
}
func GetUser(name string) *casdoorsdk.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
if owner == "" || name == "" {
return nil
}
user := casdoorsdk.User{Owner: owner, Name: name}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func getUserByEmail(email string) *casdoorsdk.User {
owner := CasdoorOrganization
if adapter == nil {
panic("casdoor adapter is nil")
}
if owner == "" || email == "" {
return nil
}
user := casdoorsdk.User{Owner: owner, Email: email}
existed, err := adapter.Engine.Get(&user)
if err != nil {
panic(err)
}
if existed {
return &user
} else {
return nil
}
}
func AddUser(user *casdoorsdk.User) bool {
if adapter == nil {
panic("casdoor adapter is nil")
}
affected, err := adapter.Engine.Insert(user)
if err != nil {
panic(err)
}
return affected != 0
}
func AddUsers(users []*casdoorsdk.User) bool {
if adapter == nil {
panic("casdoor adapter is nil")
}
if len(users) == 0 {
return false
}
affected, err := adapter.Engine.Insert(users)
if err != nil {
panic(err)
}
return affected != 0
}
func AddUsersInBatch(users []*casdoorsdk.User) bool {
batchSize := 1000
if len(users) == 0 {
return false
}
affected := false
for i := 0; i < (len(users)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
if end > len(users) {
end = len(users)
}
tmp := users[start:end]
fmt.Printf("Add users: [%d - %d].\n", start, end)
if AddUsers(tmp) {
affected = true
}
}
return affected
}
func updateUser(owner string, name string, user *casdoorsdk.User) (bool, error) {
affected, err := adapter.Engine.ID(core.PK{owner, name}).AllCols().Update(user)
if err != nil {
return false, err
}
return affected != 0, nil
}
func UpdateUser(owner string, name string, user *casdoorsdk.User) bool {
if adapter == nil {
panic("casdoor adapter is nil")
}
var affected bool
var err error
times := 0
for {
affected, err = updateUser(owner, name, user)
if err != nil {
times += 1
time.Sleep(3 * time.Second)
if times >= 10 {
panic(err)
}
} else {
break
}
}
return affected
}

22
conf/app.conf Normal file
View File

@ -0,0 +1,22 @@
appname = casnode
httpport = 7000
runmode = dev
SessionOn = true
copyrequestbody = true
redisEndpoint =
driverName = mysql
dataSourceName = root:123@tcp(localhost:3306)/
dbName = casnode
domain = "forum.casbin.com"
casdoorDbName = casdoor
casdoorOrganization = "casbin"
casdoorApplication = "app-casnode"
casdoorStorageEndpoint = "https://cdn.casbin.com/"
casdoorEndpoint = http://localhost:8000
clientId = 014ae4bd048734ca2dea
clientSecret = xxx
httpProxy = "127.0.0.1:10808"
initScore = 2000
enableNestedReply = true
cacheExpireSeconds = 60
chromeCtxNum = 1

124
controllers/account.go Normal file
View File

@ -0,0 +1,124 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
_ "embed"
"strings"
"github.com/astaxie/beego"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
//go:embed token_jwt_key.pem
var JwtPublicKey string
func init() {
InitAuthConfig()
}
func InitAuthConfig() {
casdoorEndpoint := strings.TrimRight(beego.AppConfig.String("casdoorEndpoint"), "/")
clientId := beego.AppConfig.String("clientId")
clientSecret := beego.AppConfig.String("clientSecret")
casdoorOrganization := beego.AppConfig.String("casdoorOrganization")
casdoorApplication := beego.AppConfig.String("casdoorApplication")
casdoorsdk.InitConfig(casdoorEndpoint, clientId, clientSecret, JwtPublicKey, casdoorOrganization, casdoorApplication)
}
// @Title Signin
// @Description sign in as a member
// @Param code QueryString string true "The code to sign in"
// @Param state QueryString string true "The state"
// @Success 200 {object} controllers.api_controller.Response The Response object
// @router /signin [post]
// @Tag Account API
func (c *ApiController) Signin() {
code := c.Input().Get("code")
state := c.Input().Get("state")
token, err := casdoorsdk.GetOAuthToken(code, state)
if err != nil {
c.ResponseError(err.Error())
return
}
claims, err := casdoorsdk.ParseJwtToken(token.AccessToken)
if err != nil {
c.ResponseError(err.Error())
return
}
affected, err := object.UpdateMemberOnlineStatus(&claims.User, true, util.GetCurrentTime())
if err != nil {
c.ResponseError(err.Error())
return
}
claims.AccessToken = token.AccessToken
c.SetSessionClaims(claims)
c.ResponseOk(claims, affected)
}
// @Title Signout
// @Description sign out the current member
// @Success 200 {object} controllers.api_controller.Response The Response object
// @router /signout [post]
// @Tag Account API
func (c *ApiController) Signout() {
claims := c.GetSessionClaims()
if claims != nil {
_, err := object.UpdateMemberOnlineStatus(&claims.User, false, util.GetCurrentTime())
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.SetSessionClaims(nil)
c.ResponseOk()
}
// @Title GetAccount
// @Description Get current account
// @Success 200 {object} controllers.api_controller.Response The Response object
// @router /get-account [get]
// @Tag Account API
func (c *ApiController) GetAccount() {
if c.RequireSignedIn() {
return
}
claims := c.GetSessionClaims()
c.ResponseOk(claims)
}
func (c *ApiController) UpdateAccountBalance(amount int) {
user := c.GetSessionUser()
user.Score += amount
c.SetSessionUser(user)
}
func (c *ApiController) UpdateAccountConsumptionSum(amount int) {
user := c.GetSessionUser()
user.Karma += amount
c.SetSessionUser(user)
}

205
controllers/balance.go Normal file
View File

@ -0,0 +1,205 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"math/rand"
"time"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
// @Tag Balance API
// @Title AddThanks
// @router /add-thanks [post]
func (c *ApiController) AddThanks() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
id := util.ParseInt(c.Input().Get("id"))
thanksType := c.Input().Get("thanksType") // 1 means topic, 2 means reply
var author *casdoorsdk.User
if thanksType == "2" {
author = object.GetReplyAuthor(id)
} else {
author = object.GetTopicAuthor(id)
}
consumerRecord := object.ConsumptionRecord{
ConsumerId: author.Name,
ReceiverId: GetUserName(user),
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
}
receiverRecord := object.ConsumptionRecord{
ConsumerId: GetUserName(user),
ReceiverId: author.Name,
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
}
if thanksType == "2" || thanksType == "1" {
if thanksType == "2" {
consumerRecord.Amount = -object.ReplyThanksCost
receiverRecord.Amount = object.ReplyThanksCost
consumerRecord.ConsumptionType = 5
receiverRecord.ConsumptionType = 3
} else {
consumerRecord.Amount = -object.TopicThanksCost
receiverRecord.Amount = object.TopicThanksCost
consumerRecord.ConsumptionType = 4
receiverRecord.ConsumptionType = 2
}
consumerRecord.Balance = object.GetMemberBalance(user) + consumerRecord.Amount
if consumerRecord.Balance < 0 {
c.ResponseError("You don't have enough balance.")
return
}
receiverRecord.Balance = object.GetMemberBalance(user) + receiverRecord.Amount
object.AddBalance(&receiverRecord)
object.AddBalance(&consumerRecord)
_, err := object.UpdateMemberBalance(user, consumerRecord.Amount)
if err != nil {
c.ResponseError(err.Error())
return
}
_, err = object.UpdateMemberBalance(user, receiverRecord.Amount)
if err != nil {
c.ResponseError(err.Error())
return
}
if thanksType == "2" {
object.AddReplyThanksNum(id)
}
c.UpdateAccountBalance(consumerRecord.Amount)
_, err = object.UpdateMemberConsumptionSum(user, -consumerRecord.Amount)
if err != nil {
c.ResponseError(err.Error())
return
}
c.UpdateAccountConsumptionSum(-consumerRecord.Amount)
c.ResponseOk()
} else {
c.ResponseError(fmt.Sprintf("wrong thanksType: %s", thanksType))
}
}
// @Tag Balance API
// @Title GetConsumptionRecord
// @router /get-consumption-record [get]
func (c *ApiController) GetConsumptionRecord() {
username := c.GetSessionUsername()
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultBalancePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
res := object.GetMemberConsumptionRecord(username, limit, offset)
num := object.GetMemberConsumptionRecordNum(username)
c.ResponseOk(res, num)
}
// @Tag Balance API
// @Title GetCheckinBonus
// @router /get-checkin-bonus [get]
func (c *ApiController) GetCheckinBonus() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
checkinDate := object.GetMemberCheckinDate(user)
date := util.GetDateStr()
if date == checkinDate {
c.ResponseError("You have received the daily checkin bonus today.")
return
}
maxBonus := object.MaxDailyCheckinBonus
rand.Seed(time.Now().UnixNano())
bonus := rand.Intn(maxBonus)
record := object.ConsumptionRecord{
// Id: util.IntToString(object.GetConsumptionRecordId() + 1),
Amount: bonus,
Balance: object.GetMemberBalance(user) + bonus,
ReceiverId: GetUserName(user),
CreatedTime: util.GetCurrentTime(),
ConsumptionType: 1,
}
object.AddBalance(&record)
_, err := object.UpdateMemberBalance(user, bonus)
if err != nil {
c.ResponseError(err.Error())
return
}
_, err = object.UpdateMemberCheckinDate(user, date)
if err != nil {
c.ResponseError(err.Error())
return
}
c.UpdateAccountBalance(record.Amount)
c.ResponseOk(bonus)
}
// @router /get-checkin-bonus-status [get]
// @Tag Balance API
// @Title GetCheckinBonusStatus
func (c *ApiController) GetCheckinBonusStatus() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
checkinDate := object.GetMemberCheckinDate(user)
date := util.GetDateStr()
res := checkinDate == date
c.ResponseOk(res, date)
}

88
controllers/base.go Normal file
View File

@ -0,0 +1,88 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/gob"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
type ApiController struct {
beego.Controller
}
func init() {
gob.Register(casdoorsdk.Claims{})
}
func GetUserName(user *casdoorsdk.User) string {
if user == nil {
return ""
}
return user.Name
}
func (c *ApiController) GetSessionClaims() *casdoorsdk.Claims {
s := c.GetSession("user")
if s == nil {
return nil
}
claims := s.(casdoorsdk.Claims)
return &claims
}
func (c *ApiController) SetSessionClaims(claims *casdoorsdk.Claims) {
if claims == nil {
c.DelSession("user")
return
}
c.SetSession("user", *claims)
}
func (c *ApiController) GetSessionUser() *casdoorsdk.User {
claims := c.GetSessionClaims()
if claims == nil {
return nil
}
return &claims.User
}
func (c *ApiController) SetSessionUser(user *casdoorsdk.User) {
if user == nil {
// c.DelSession("user")
return
}
claims := c.GetSessionClaims()
if claims != nil {
claims.User = *user
c.SetSessionClaims(claims)
}
}
func (c *ApiController) GetSessionUsername() string {
user := c.GetSessionUser()
if user == nil {
return ""
}
return GetUserName(user)
}

247
controllers/favorites.go Normal file
View File

@ -0,0 +1,247 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"sync"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
// @router /add-favorites [post]
// @Tag Favorite API
// @Title AddFavorites
func (c *ApiController) AddFavorites() {
objectId := c.Input().Get("id")
favoritesType := c.Input().Get("type")
memberId := c.GetSessionUsername()
var resp Response
if object.IsFavoritesExist(favoritesType) == false {
c.ResponseError("Invalid favorites type")
return
}
favoriteStatus := object.GetFavoritesStatus(memberId, objectId, favoritesType)
if favoriteStatus {
c.ResponseOk(resp)
return
}
username := c.GetSessionUsername()
favorites := object.Favorites{
// Id: util.IntToString(object.GetFavoritesCount()) + username,
FavoritesType: favoritesType,
ObjectId: objectId,
CreatedTime: util.GetCurrentTime(),
MemberId: username,
}
var wg sync.WaitGroup
res := true
if favorites.FavoritesType == object.FavorTopic {
wg.Add(1)
go func() {
topicId := util.ParseInt(favorites.ObjectId)
res = object.ChangeTopicFavoriteCount(topicId, 1)
wg.Done()
}()
}
if favorites.FavoritesType == object.SubscribeTopic {
wg.Add(1)
go func() {
topicId := util.ParseInt(favorites.ObjectId)
res = object.ChangeTopicSubscribeCount(topicId, 1)
wg.Done()
}()
}
res = object.AddFavorites(&favorites)
if favoritesType == object.FavorTopic {
topicId := util.ParseInt(objectId)
notification := object.Notification{
// Id: util.IntToString(object.GetNotificationId()),
NotificationType: 4,
ObjectId: topicId,
CreatedTime: util.GetCurrentTime(),
SenderId: c.GetSessionUsername(),
ReceiverId: object.GetTopicAuthor(topicId).Name,
Status: 1,
}
if notification.ReceiverId != notification.SenderId {
_ = object.AddNotification(&notification)
}
}
resp = Response{Status: "ok", Msg: "success", Data: res}
wg.Wait()
if !res {
c.ResponseError("add favorite wrong")
return
}
c.ResponseOk(resp)
}
// @router /delete-favorites [post]
// @Tag Favorite API
// @Title DeleteFavorites
func (c *ApiController) DeleteFavorites() {
memberId := c.GetSessionUsername()
objectId := c.Input().Get("id")
favoritesType := c.Input().Get("type")
var resp Response
if object.IsFavoritesExist(favoritesType) == false {
resp = Response{Status: "fail", Msg: "param wrong"}
c.Data["json"] = resp
c.ServeJSON()
}
var wg sync.WaitGroup
res := true
if favoritesType == object.FavorTopic {
topicId := util.ParseInt(objectId)
wg.Add(1)
go func() {
res = object.ChangeTopicFavoriteCount(topicId, -1)
wg.Done()
}()
}
if favoritesType == object.SubscribeTopic {
topicId := util.ParseInt(objectId)
wg.Add(1)
go func() {
res = object.ChangeTopicSubscribeCount(topicId, -1)
wg.Done()
}()
}
res = object.DeleteFavorites(memberId, objectId, favoritesType)
resp = Response{Status: "ok", Msg: "success", Data: res}
wg.Wait()
if !res {
resp = Response{Status: "fail", Msg: "delete favorite wrong"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @router /get-favorites-status [get]
// @Tag Favorite API
// @Title GetFavoritesStatus
func (c *ApiController) GetFavoritesStatus() {
memberId := c.GetSessionUsername()
objectId := c.Input().Get("id")
favoritesType := c.Input().Get("type")
var resp Response
if object.IsFavoritesExist(favoritesType) {
res := object.GetFavoritesStatus(memberId, objectId, favoritesType)
resp = Response{Status: "ok", Msg: "success", Data: res}
} else {
resp = Response{Status: "fail", Msg: "param wrong"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @router /get-favorites [get]
// @Tag Favorite API
// @Title GetFavorites
func (c *ApiController) GetFavorites() {
memberId := c.GetSessionUsername()
favoritesType := c.Input().Get("type")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultPageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
var resp Response
switch favoritesType {
case object.FavorTopic:
res := object.GetTopicsFromFavorites(memberId, limit, offset, object.FavorTopic)
num := object.GetFavoritesNum(object.FavorTopic, memberId)
resp = Response{Status: "ok", Msg: "success", Data: res, Data2: num}
break
case object.FollowUser:
res := object.GetFollowingNewAction(memberId, limit, offset)
num := object.GetFavoritesNum(object.FollowUser, memberId)
resp = Response{Status: "ok", Msg: "success", Data: res, Data2: num}
break
case object.FavorNode:
res := object.GetNodesFromFavorites(memberId, limit, offset)
num := object.GetFavoritesNum(object.FavorNode, memberId)
resp = Response{Status: "ok", Msg: "success", Data: res, Data2: num}
break
case object.SubscribeTopic:
res := object.GetTopicsFromFavorites(memberId, limit, offset, object.SubscribeTopic)
num := object.GetFavoritesNum(object.SubscribeTopic, memberId)
resp = Response{Status: "ok", Msg: "success", Data: res, Data2: num}
break
default:
resp = Response{Status: "fail", Msg: "param wrong"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @router /get-account-favorite-num [get]
// @Tag Favorite API
// @Title GetAccountFavoriteNum
func (c *ApiController) GetAccountFavoriteNum() {
memberId := c.GetSessionUsername()
var res [6]int
var wg sync.WaitGroup
// favorite type set,5 object.favorTopic...
typeSet := []string{object.FavorTopic, object.FollowUser, object.FavorNode, object.SubscribeTopic}
for i := 1; i <= len(typeSet); i++ {
wg.Add(1)
i := i
go func() {
if i == 2 {
res[i] = object.GetFollowingNum(memberId)
} else {
res[i] = object.GetFavoritesNum(typeSet[i-1], memberId)
}
wg.Done()
}()
}
wg.Wait()
c.ResponseOk(res)
}

240
controllers/file.go Normal file
View File

@ -0,0 +1,240 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
)
type NewUploadFile struct {
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
FileUrl string `json:"fileUrl"`
Size int `json:"size"`
}
func (c *ApiController) GetFiles() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultFilePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
files := object.GetFiles(GetUserName(user), limit, offset)
fileNum := fileNumResp{Num: object.GetFilesNum(GetUserName(user)), MaxNum: object.GetMemberFileQuota(user)}
c.ResponseOk(files, fileNum)
}
func (c *ApiController) GetFileNum() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
num := fileNumResp{Num: object.GetFilesNum(GetUserName(user)), MaxNum: object.GetMemberFileQuota(user)}
resp := Response{Status: "ok", Msg: "success", Data: num}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) AddFileRecord() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
var file NewUploadFile
err := json.Unmarshal(c.Ctx.Input.RequestBody, &file)
if err != nil {
panic(err)
}
var resp Response
uploadFileNum := object.GetFilesNum(GetUserName(user))
if uploadFileNum >= object.GetMemberFileQuota(user) {
resp = Response{Status: "fail", Msg: "You have exceeded the upload limit."}
c.Data["json"] = resp
c.ServeJSON()
return
}
record := object.UploadFileRecord{
FileName: file.FileName,
FilePath: file.FilePath,
FileUrl: file.FileUrl,
FileType: util.FileType(file.FileName),
FileExt: util.FileExt(file.FileName),
MemberId: GetUserName(user),
CreatedTime: util.GetCurrentTime(),
Size: file.Size,
Deleted: false,
}
affected, id := object.AddFileRecord(&record)
if affected {
fileNum := fileNumResp{Num: object.GetFilesNum(GetUserName(user)), MaxNum: object.GetMemberFileQuota(user)}
resp = Response{Status: "ok", Msg: "success", Data: id, Data2: fileNum}
} else {
resp = Response{Status: "fail", Msg: "Add file failed, please try again.", Data: id}
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) DeleteFile() {
idStr := c.Input().Get("id")
user := c.GetSessionUser()
id := util.ParseInt(idStr)
fileInfo := object.GetFile(id)
if !object.FileEditable(user, fileInfo.MemberId) {
c.ResponseError("Permission denied.")
return
}
affected := object.DeleteFileRecord(id)
var resp Response
if affected {
service.DeleteFileFromStorage(fileInfo.FilePath)
fileNum := fileNumResp{Num: object.GetFilesNum(GetUserName(user)), MaxNum: object.GetMemberFileQuota(user)}
resp = Response{Status: "ok", Msg: "success", Data: id, Data2: fileNum}
} else {
resp = Response{Status: "fail", Msg: "Delete file failed, please try again."}
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) GetFile() {
idStr := c.Input().Get("id")
id := util.ParseInt(idStr)
file := object.GetFile(id)
var resp Response
if file == nil || file.Deleted {
resp = Response{Status: "error", Msg: "No such file."}
} else {
object.AddFileViewsNum(id) // together with add file views num
resp = Response{Status: "ok", Msg: "success", Data: file}
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) UpdateFileDescribe() {
user := c.GetSessionUser()
id := util.ParseInt(c.Input().Get("id"))
var desc fileDescribe
err := json.Unmarshal(c.Ctx.Input.RequestBody, &desc)
if err != nil {
panic(err)
}
var resp Response
file := object.GetFile(id)
if !object.FileEditable(user, file.MemberId) {
resp = Response{Status: "fail", Msg: "Permission denied."}
c.Data["json"] = resp
c.ServeJSON()
return
} else {
res := object.UpdateFileDescribe(id, desc.FileName, desc.Desc)
resp = Response{Status: "ok", Msg: "success", Data: res}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title UploadFile
// @Tag File API
// @router /upload-file [post]
func (c *ApiController) UploadFile() {
if c.RequireSignedIn() {
return
}
memberId := c.GetSessionUsername()
fileBase64 := c.Ctx.Request.Form.Get("file")
fileType := c.Ctx.Request.Form.Get("type")
fileName := c.Ctx.Request.Form.Get("name")
index := strings.Index(fileBase64, ",")
fileBytes, _ := base64.StdEncoding.DecodeString(fileBase64[index+1:])
fileUrl, _ := service.UploadFileToStorage(memberId, "file", "UploadFile", fmt.Sprintf("casnode/file/%s/%s.%s", memberId, fileName, fileType), fileBytes)
resp := Response{Status: "ok", Msg: fileName + "." + fileType, Data: fileUrl}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title ModeratorUpload
// @Tag File API
// @router /upload-moderator [post]
func (c *ApiController) ModeratorUpload() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
if !user.IsAdmin {
c.ResponseError("You have no permission to upload files here. Need to be moderator.")
return
}
fileBase64 := c.Ctx.Request.Form.Get("file")
fileName := c.Ctx.Request.Form.Get("name")
filePath := c.Ctx.Request.Form.Get("filepath")
index := strings.Index(fileBase64, ",")
fileBytes, _ := base64.StdEncoding.DecodeString(fileBase64[index+1:])
fileUrl, _ := service.UploadFileToStorage(user.Name, "file", "ModeratorUpload", fmt.Sprintf("casnode/file/%s/%s/%s", user.Name, filePath, fileName), fileBytes)
timeStamp := fmt.Sprintf("?time=%d", time.Now().UnixNano())
c.ResponseOk(fileUrl + timeStamp)
// resp := Response{Status: "ok", Msg: fileName, Data: fileUrl + timeStamp}
}

101
controllers/frontConf.go Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
)
// @Title GetFrontConfById
// @Description Get front conf by id
// @Success 200 {object} object.FrontConf The Response object
// @router /get-front-conf-by-id [get]
// @Tag FrontConf API
func (c *ApiController) GetFrontConfById() {
id := c.Input().Get("id")
conf := object.GetFrontConfById(id)
c.ResponseOk(conf)
}
// @Title GetFrontConfsByField
// @Description Get front confs by field
// @Success 200 {array} object.FrontConf The Response object
// @router /get-front-confs-by-field [get]
// @Tag FrontConf API
func (c *ApiController) GetFrontConfsByField() {
field := c.Input().Get("field")
confs := object.GetFrontConfsByField(field)
c.ResponseOk(confs)
}
// @router /update-front-conf-by-id [post]
// @Tag FrontConf API
// @Title UpdateFrontConfById
func (c *ApiController) UpdateFrontConfById() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
// get from body
var value string
err := json.Unmarshal(c.Ctx.Input.RequestBody, &value)
tags := service.Finalword(value)
affect, err := object.UpdateFrontConfById(id, value, tags)
if err != nil {
c.ResponseError(err.Error())
}
c.ResponseOk(affect)
}
// @router /update-front-confs-by-filed [post]
// @Tag FrontConf API
// @Title UpdateFrontConfsByField
func (c *ApiController) UpdateFrontConfsByField() {
if c.RequireAdmin() {
return
}
filed := c.Input().Get("field")
var confs []*object.FrontConf
err := json.Unmarshal(c.Ctx.Input.RequestBody, &confs)
if err != nil {
c.ResponseError(err.Error())
}
err = object.UpdateFrontConfsByField(confs, filed)
if err != nil {
c.ResponseError(err.Error())
}
c.ResponseOk(nil)
}
// @router /restore-front-confs [post]
// @Tag FrontConf API
// @Title RestoreFrontConfs
func (c *ApiController) RestoreFrontConfs() {
if c.RequireAdmin() {
return
}
filed := c.Input().Get("field")
res := object.UpdateFrontConfsByField(object.Confs, filed)
c.ResponseOk(res)
}

52
controllers/hot.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
// @Tag Hot API
// @Title ChangeExpiredDataStatus
// @router /update-expired-data [post]
func (c *ApiController) ChangeExpiredDataStatus() {
expiredNodeDate := util.GetTimeMonth(-object.NodeHitRecordExpiredTime)
expiredTopicDate := util.GetTimeDay(-object.TopicHitRecordExpiredTime)
updateNodeNum := object.ChangeExpiredDataStatus(1, expiredNodeDate)
updateTopicNum := object.ChangeExpiredDataStatus(2, expiredTopicDate)
c.Data["json"] = Response{Status: "ok", Data: updateNodeNum, Data2: updateTopicNum}
c.ServeJSON()
}
// @Tag Hot API
// @Title UpdateHotInfo
// @router /update-hot-info [post]
func (c *ApiController) UpdateHotInfo() {
var updateNodeNum int
var updateTopicNum int
last := object.GetLastRecordId()
latest := object.GetLatestSyncedRecordId()
if last != latest {
object.UpdateLatestSyncedRecordId(last)
updateNodeNum = object.UpdateHotNode(latest)
updateTopicNum = object.UpdateHotTopic(latest)
}
c.Data["json"] = Response{Status: "ok", Data: updateNodeNum, Data2: updateTopicNum}
c.ServeJSON()
}

93
controllers/info.go Normal file
View File

@ -0,0 +1,93 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"sync"
"github.com/casbin/casnode/object"
)
// @Tag Info API
// @Title GetCommunityHealth
// @router /get-community-health [get]
func (c *ApiController) GetCommunityHealth() {
var memberCount int
var topicCount int
var replyCount int
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
memberCount = object.GetMemberNum()
}()
go func() {
defer wg.Done()
topicCount = object.GetTopicCount()
}()
go func() {
defer wg.Done()
replyCount = object.GetReplyCount()
}()
wg.Wait()
res := object.CommunityHealth{
Member: memberCount,
Topic: topicCount,
Reply: replyCount,
}
c.ResponseOk(res)
}
// @Tag Info API
// @Title GetForumVersion
// @router /get-forum-version [get]
func (c *ApiController) GetForumVersion() {
var resp Response
res := object.GetForumVersion()
resp = Response{Status: "ok", Msg: "success", Data: res}
c.Data["json"] = resp
c.ServeJSON()
}
// @Tag Info API
// @Title GetOnlineNum
// @router /get-online-num [get]
func (c *ApiController) GetOnlineNum() {
onlineNum := object.GetOnlineMemberNum()
highest := object.GetHighestOnlineNum()
c.ResponseOk(onlineNum, highest)
}
// @Tag Info API
// @Title GetNodeNavigation
// @router /node-navigation [get]
func (c *ApiController) GetNodeNavigation() {
var resp Response
res := object.GetNodeNavigation()
resp = Response{Status: "ok", Msg: "success", Data: res}
c.Data["json"] = resp
c.ServeJSON()
}

145
controllers/member.go Normal file
View File

@ -0,0 +1,145 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"fmt"
"github.com/casbin/casnode/object"
)
// @Title GetMember
// @Description get member by id
// @Param id query string true "id"
// @Success 200 {object} casdoorsdk.User The Response object
// @router /get-member [get]
// @Tag Member API
func (c *ApiController) GetMember() {
id := c.Input().Get("id")
c.ResponseOk(object.GetUser(id))
}
// @Title GetMemberEditorType
// @Description member editortype
// @Success 200 {object} controllers.Response The Response object
// @router /get-member-editor-type [get]
// @Tag Member API
func (c *ApiController) GetMemberEditorType() {
user := c.GetSessionUser()
editorType := ""
if user != nil {
editorType = object.GetMemberEditorType(user)
}
c.ResponseOk(editorType)
}
// @Title GetRankingRich
// @Description RankingRich
// @Success 200 {array} casdoorsdk.User The Response object
// @router /get-ranking-rich [get]
// @Tag Member API
func (c *ApiController) GetRankingRich() {
users, err := object.GetRankingRich()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(users)
}
// @Title GetRankingPlayer
// @Description RankingPlayer
// @Success 200 {array} casdoorsdk.User The Response object
// @router /get-ranking-player [get]
// @Tag Member API
func (c *ApiController) GetRankingPlayer() {
users, err := object.GetRankingPlayer()
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(users)
}
// @Tag Member API
// @Title UpdateMemberEditorType
// @router /update-member-editor-type [post]
func (c *ApiController) UpdateMemberEditorType() {
editorType := c.Input().Get("editorType")
user := c.GetSessionUser()
if editorType != "markdown" && editorType != "richtext" {
c.ResponseError(fmt.Errorf("unsupported editor type: %s", editorType).Error())
return
}
affected, err := object.UpdateMemberEditorType(user, editorType)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(affected)
}
// @Tag Member API
// @Title UpdateMemberLanguage
// @router /update-member-language [post]
func (c *ApiController) UpdateMemberLanguage() {
language := c.Input().Get("language")
if language != "zh" && language != "en" {
c.ResponseError(fmt.Errorf("unsupported language: %s", language).Error())
return
}
user := c.GetSessionUser()
if user == nil {
c.ResponseOk()
return
}
user.Language = language
c.SetSessionUser(user)
affected, err := object.UpdateMemberLanguage(user, language)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(affected)
}
// @Title GetMemberLanguage
// @Description MemberLanguage
// @Success 200 {object} controllers.Response The Response object
// @router /get-member-language [get]
// @Tag Member API
func (c *ApiController) GetMemberLanguage() {
user := c.GetSessionUser()
language := ""
if user != nil {
language = object.GetMemberLanguage(user)
}
c.ResponseOk(language)
}

270
controllers/node.go Normal file
View File

@ -0,0 +1,270 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
// @Title GetNodes
// @router /get-nodes [get]
// @Tag Node API
func (c *ApiController) GetNodes() {
c.Data["json"] = object.GetNodes()
c.ServeJSON()
}
// @Title GetNodesAdmin
// @router /get-nodes-admin [get]
// @Tag Node API
func (c *ApiController) GetNodesAdmin() {
res := []adminNodeInfo{}
nodes := object.GetNodes()
for _, v := range nodes {
node := adminNodeInfo{
NodeInfo: *v,
TopicNum: object.GetNodeTopicNum(v.Id),
FavoritesNum: object.GetNodeFavoritesNum(v.Id),
}
res = append(res, node)
}
c.Data["json"] = res
c.ServeJSON()
}
// @Title GetNode
// @router /get-node [get]
// @Tag Node API
func (c *ApiController) GetNode() {
id := c.Input().Get("id")
c.Data["json"] = object.GetNode(id)
c.ServeJSON()
}
// @Title UpdateNode
// @router /update-node [post]
// @Tag Node API
func (c *ApiController) UpdateNode() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
var node object.Node
err := json.Unmarshal(c.Ctx.Input.RequestBody, &node)
if err != nil {
panic(err)
}
res := object.UpdateNode(id, &node)
c.ResponseOk(res)
}
// @Title AddNode
// @router /add-node [post]
// @Tag Node API
func (c *ApiController) AddNode() {
if c.RequireAdmin() {
return
}
var node object.Node
var resp Response
err := json.Unmarshal(c.Ctx.Input.RequestBody, &node)
if err != nil {
panic(err)
}
if node.Id == "" || node.Name == "" || node.TabId == "" || node.PlaneId == "" {
resp = Response{Status: "fail", Msg: "Some information is missing"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.HasNode(node.Id) {
resp = Response{Status: "fail", Msg: "Node ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
node.CreatedTime = util.GetCurrentTime()
res := object.AddNode(&node)
c.ResponseOk(res)
}
// @Title DeleteNode
// @router /delete-node [post]
// @Tag Node API
func (c *ApiController) DeleteNode() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
c.Data["json"] = object.DeleteNode(id)
c.ServeJSON()
}
// @Title GetNodesNum
// @router /get-nodes-num [get]
// @Tag Node API
func (c *ApiController) GetNodesNum() {
num := object.GetNodesNum()
c.ResponseOk(num)
}
// @Title GetNodeInfo
// @router /get-node-info [get]
// @Tag Node API
func (c *ApiController) GetNodeInfo() {
id := c.Input().Get("id")
num := object.GetNodeTopicNum(id)
favoriteNum := object.GetNodeFavoritesNum(id)
c.ResponseOk(num, favoriteNum)
}
func (c *ApiController) GetNodeFromTab() {
tab := c.Input().Get("tab")
nodes := object.GetNodeFromTab(tab)
c.ResponseOk(nodes)
}
// @Title GetNodeRelation
// @router /get-node-relation [get]
// @Tag Node API
func (c *ApiController) GetNodeRelation() {
id := c.Input().Get("id")
res := object.GetNodeRelation(id)
c.ResponseOk(res)
}
// @Tag Node API
// @router /get-latest-node [get]
// @Title GetLatestNode
func (c *ApiController) GetLatestNode() {
limitStr := c.Input().Get("limit")
defaultLimit := object.LatestNodeNum
var limit int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
res := object.GetLatestNode(limit)
c.ResponseOk(res)
}
// @Title GetHotNod
// @Tag Node API
// @router /get-hot-node [get]
func (c *ApiController) GetHotNode() {
limitStr := c.Input().Get("limit")
defaultLimit := object.HotNodeNum
var limit int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
res := object.GetHotNode(limit)
c.ResponseOk(res)
}
// @Title AddNodeBrowseCount
// @Tag Node API
// @router /add-node-browse-record [post]
func (c *ApiController) AddNodeBrowseCount() {
nodeId := c.Input().Get("id")
var resp Response
hitRecord := object.BrowseRecord{
MemberId: c.GetSessionUsername(),
RecordType: 1,
ObjectId: nodeId,
CreatedTime: util.GetCurrentTime(),
Expired: false,
}
res := object.AddBrowseRecordNum(&hitRecord)
if res {
c.ResponseOk()
} else {
resp = Response{Status: "fail", Msg: "add node hit count failed"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title AddNodeModerators
// @Tag Node API
// @router /add-node-moderators [post]
func (c *ApiController) AddNodeModerators() {
if c.RequireAdmin() {
return
}
var moderators addNodeModerator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &moderators)
if err != nil {
panic(err)
}
moderator := object.GetUser(moderators.MemberId)
if moderator == nil {
c.ResponseError("Member doesn't exist.")
return
}
res := object.AddNodeModerators(moderators.MemberId, moderators.NodeId)
if res {
c.ResponseOk(res)
} else {
c.ResponseError("Moderator already exist.")
}
}
// @Title DeleteNodeModerators
// @Tag Node API
// @router /delete-node-moderators [post]
func (c *ApiController) DeleteNodeModerators() {
if c.RequireAdmin() {
return
}
var moderators deleteNodeModerator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &moderators)
if err != nil {
panic(err)
}
res := object.DeleteNodeModerators(moderators.MemberId, moderators.NodeId)
c.ResponseOk(res)
}

View File

@ -0,0 +1,111 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
func (c *ApiController) AddNotification() {
var tempNotification newNotification
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tempNotification)
if err != nil {
panic(err)
}
memberId := c.GetSessionUsername()
notification := object.Notification{
// Id: util.IntToString(object.GetNotificationId()),
NotificationType: tempNotification.NotificationType,
ObjectId: tempNotification.ObjectId,
CreatedTime: util.GetCurrentTime(),
SenderId: memberId,
ReceiverId: tempNotification.ReceiverId,
Status: 1,
}
var resp Response
if notification.NotificationType <= 6 && notification.NotificationType >= 1 {
res := object.AddNotification(&notification)
if !res {
resp = Response{Status: "fail", Msg: "add notification wrong"}
} else {
resp = Response{Status: "ok", Msg: "success", Data: res}
}
} else {
resp = Response{Status: "fail", Msg: "param wrong"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @router /get-notifications [get]
// @Title GetNotifications
// @Tag Notification API
func (c *ApiController) GetNotifications() {
memberId := c.GetSessionUsername()
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultNotificationPageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
res := object.GetNotifications(memberId, limit, offset)
num := object.GetNotificationNum(memberId)
c.ResponseOk(res, num)
}
// @Title DeleteNotification
// @router /delete-notifications [get]
// @Tag Notification API
func (c *ApiController) DeleteNotification() {
id := c.Input().Get("id")
res := object.DeleteNotification(id)
c.ResponseOk(res)
}
// @Title GetUnreadNotificationNum
// @router /get-unread-notification-num [post]
// @Tag Notification API
func (c *ApiController) GetUnreadNotificationNum() {
memberId := c.GetSessionUsername()
res := object.GetUnreadNotificationNum(memberId)
c.ResponseOk(res)
}
// @Title UpdateReadStatus
// @router /update-read-status [post]
// @Tag Notification API
func (c *ApiController) UpdateReadStatus() {
memberId := c.GetSessionUsername()
c.Data["json"] = object.UpdateReadStatus(memberId)
c.ServeJSON()
}

150
controllers/plane.go Normal file
View File

@ -0,0 +1,150 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
func (c *ApiController) GetPlanes() {
c.Data["json"] = object.GetPlanes()
c.ServeJSON()
}
// @Title GetPlanesAdmin
// @router /get-planes-admin [get]
// @Tag Plane API
func (c *ApiController) GetPlanesAdmin() {
c.Data["json"] = object.GetAllPlanes()
c.ServeJSON()
}
// @Title GetPlane
// @router /get-plane [get]
// @Tag Plane API
func (c *ApiController) GetPlane() {
id := c.Input().Get("id")
c.Data["json"] = object.GetPlane(id)
c.ServeJSON()
}
// @Title GetPlaneAdmin
// @router /get-planes-admin [get]
// @Tag Plane API
func (c *ApiController) GetPlaneAdmin() {
id := c.Input().Get("id")
c.Data["json"] = object.GetPlaneAdmin(id)
c.ServeJSON()
}
// @Title GetPlaneLis
// @router /get-plane-list [get]
// @Tag Plane API
func (c *ApiController) GetPlaneList() {
c.ResponseOk(object.GetPlaneList())
}
// @Title AddPlane
// @router /add-plane [post]
// @Tag Plane API
func (c *ApiController) AddPlane() {
if c.RequireAdmin() {
return
}
var plane object.AdminPlaneInfo
var resp Response
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plane)
if err != nil {
panic(err)
}
if plane.Id == "" || plane.Name == "" {
resp = Response{Status: "fail", Msg: "Some information is missing"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.HasPlane(plane.Id) {
resp = Response{Status: "fail", Msg: "Plane ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
newPlane := object.Plane{
Id: plane.Id,
Name: plane.Name,
Sorter: plane.Sorter,
CreatedTime: util.GetCurrentTime(),
Image: plane.Image,
BackgroundColor: plane.BackgroundColor,
Color: plane.Color,
Visible: plane.Visible,
}
res := object.AddPlane(&newPlane)
c.ResponseOk(res)
}
// @Title UpdatePlane
// @router /update-plane [post]
// @Tag Plane API
func (c *ApiController) UpdatePlane() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
var plane object.AdminPlaneInfo
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plane)
if err != nil {
panic(err)
}
newPlane := object.Plane{
Id: plane.Id,
Name: plane.Name,
Sorter: plane.Sorter,
CreatedTime: plane.CreatedTime,
Image: plane.Image,
BackgroundColor: plane.BackgroundColor,
Color: plane.Color,
Visible: plane.Visible,
}
res := object.UpdatePlane(id, &newPlane)
c.ResponseOk(res)
}
// @Title DeletePlane
// @router /delete-plane [post]
// @Tag Plane API
func (c *ApiController) DeletePlane() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
c.Data["json"] = Response{Status: "ok", Msg: "success", Data: object.DeletePlane(id)}
c.ServeJSON()
}

56
controllers/poster.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
)
// @Title UpdatePoster
// @Description update poster message
// @Success 200 {object} controllers.Response The Response object
// @router /update-poster [post]
// @Tag Poster API
func (c *ApiController) UpdatePoster() {
if c.RequireAdmin() {
return
}
var tempposter object.Poster
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tempposter)
if err != nil {
panic(err)
}
object.UpdatePoster(tempposter.Id, tempposter)
c.Data["json"] = Response{Status: "ok", Msg: "success"}
c.ServeJSON()
}
// @Title ReadPoster
// @Description get poster by id
// @Param id query string true "id"
// @Success 200 {object} object.Poster The Response object
// @router /read-poster [get]
// @Tag Poster API
func (c *ApiController) ReadPoster() {
n := c.Input().Get("id")
res := object.GetPoster(n)
c.Data["json"] = res
c.ServeJSON()
}

257
controllers/reply.go Normal file
View File

@ -0,0 +1,257 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"strconv"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
)
type NewReplyForm struct {
Content string `json:"content"`
TopicId int `json:"topicId"`
}
// @Title GetReplies
// @Tag Reply API
// @router /get-replies [get]
func (c *ApiController) GetReplies() {
user := c.GetSessionUser()
topicIdStr := c.Input().Get("topicId")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
initStatus := c.Input().Get("init")
topicId := util.ParseInt(topicIdStr)
var limit, page int
repliesNum := object.GetTopicReplyNum(topicId)
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
c.Data["json"] = Response{Status: "error", Msg: "Parameter missing: limit"}
c.ServeJSON()
return
}
if len(pageStr) != 0 {
if initStatus == "false" {
page = util.ParseInt(pageStr)
} else {
page = (repliesNum-1)/limit + 1
}
}
replies, realPage := object.GetReplies(topicId, user, limit, page)
if replies == nil {
replies = []*object.ReplyWithAvatar{}
}
c.Data["json"] = Response{Status: "ok", Msg: "success", Data: replies, Data2: []int{repliesNum, realPage}}
c.ServeJSON()
}
// @Title GetAllRepliesOfTopic
// @Tag Reply API
// @router /get-replies-of-topic [get]
func (c *ApiController) GetAllRepliesOfTopic() {
topicId := util.ParseInt(c.Input().Get("topicId"))
replies := object.GetRepliesOfTopic(topicId)
c.Data["json"] = Response{Status: "ok", Msg: "success", Data: replies, Data2: len(replies)}
c.ServeJSON()
}
// @Title GetReply
// @Tag Reply API
// @router /get-reply [get]
func (c *ApiController) GetReply() {
idStr := c.Input().Get("id")
id := util.ParseInt(idStr)
c.Data["json"] = object.GetReply(id)
c.ServeJSON()
}
// @Title GetReplyWithDetails
// @Tag Reply API
// @router /get-reply-with-details [get]
func (c *ApiController) GetReplyWithDetails() {
user := c.GetSessionUser()
idStr := c.Input().Get("id")
id := util.ParseInt(idStr)
c.Data["json"] = object.GetReplyWithDetails(user, id)
c.ServeJSON()
}
// @Title UpdateReply
// @Tag Reply API
// @router /update-reply [post]
func (c *ApiController) UpdateReply() {
idStr := c.Input().Get("id")
var reply object.Reply
id := util.ParseInt(idStr)
err := json.Unmarshal(c.Ctx.Input.RequestBody, &reply)
if err != nil {
panic(err)
}
c.Data["json"] = object.UpdateReply(id, &reply)
c.ServeJSON()
}
// @Title AddReply
// @Tag Reply API
// @router /add-reply [post]
func (c *ApiController) AddReply() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
if object.IsForbidden(user) {
c.ResponseError("Your account has been forbidden to perform this operation")
return
}
balance := object.GetMemberBalance(user)
if balance < object.CreateReplyCost {
c.ResponseError("You don't have enough balance.")
return
}
reply := object.Reply{
Author: GetUserName(user),
CreatedTime: util.GetCurrentTime(),
Deleted: false,
}
err := json.Unmarshal(c.Ctx.Input.RequestBody, &reply)
if err != nil {
panic(err)
}
reply.Tags = service.Finalword(reply.Content)
previousReply := object.GetReplyByContentAndAuthor(reply.Content, reply.Author)
if previousReply != nil {
c.ResponseError("You have same reply before.")
return
}
if object.ContainsSensitiveWord(reply.Content) {
c.ResponseError("Reply contains sensitive word.")
return
}
affected, id := object.AddReply(&reply)
if affected {
object.GetReplyBonus(object.GetTopicAuthor(reply.TopicId), user, id)
object.CreateReplyConsumption(user, id)
c.UpdateAccountBalance(-object.CreateReplyCost)
c.UpdateAccountConsumptionSum(object.CreateReplyCost)
object.ChangeTopicReplyCount(reply.TopicId, 1)
object.ChangeTopicLastReplyUser(reply.TopicId, GetUserName(user), util.GetCurrentTime())
object.AddReplyNotification(reply.Author, reply.Content, id, reply.TopicId)
reply.AddReplyToMailingList()
}
c.ResponseOk(affected)
}
// @Title DeleteReply
// @Tag Reply API
// @router /delete-reply [post]
func (c *ApiController) DeleteReply() {
id := util.ParseInt(c.Input().Get("id"))
user := c.GetSessionUser()
replyInfo := object.GetReply(id)
isAdmin := object.CheckIsAdmin(user)
if !object.ReplyDeletable(replyInfo.CreatedTime, GetUserName(user), replyInfo.Author) && !isAdmin {
resp := Response{Status: "fail", Msg: "Permission denied."}
c.Data["json"] = resp
c.ServeJSON()
return
}
affected := object.DeleteReply(id)
if affected {
object.ChangeTopicReplyCount(replyInfo.TopicId, -1)
lastReply := object.GetLatestReplyInfo(replyInfo.TopicId)
if lastReply != nil {
object.ChangeTopicLastReplyUser(replyInfo.TopicId, lastReply.Author, lastReply.CreatedTime)
} else {
object.ChangeTopicLastReplyUser(replyInfo.TopicId, "", "")
}
}
c.ResponseOk(affected)
}
// @Title GetLatestReplies
// @Tag Reply API
// @router /get-latest-replies [get]
func (c *ApiController) GetLatestReplies() {
id := c.Input().Get("id")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultPageNum
var (
limit, offset int
err error
)
if len(limitStr) != 0 {
limit, err = strconv.Atoi(limitStr)
if err != nil {
panic(err)
}
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page, err := strconv.Atoi(pageStr)
if err != nil {
panic(err)
}
offset = page*limit - limit
}
c.Data["json"] = object.GetLatestReplies(id, limit, offset)
c.ServeJSON()
}
// @Title GetMemberRepliesNum
// @Tag Reply API
// @router /get-member-replies-num [get]
// @Description GetRepliesNum gets member's all replies num.
func (c *ApiController) GetMemberRepliesNum() {
id := c.Input().Get("id")
c.Data["json"] = object.GetMemberRepliesNum(id)
c.ServeJSON()
}

34
controllers/search.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casbin/casnode/object"
// @Title Search
// @router /search
// @Tag Search API
func (c *ApiController) Search() {
keyword := c.Input().Get("keyword")
if len(keyword) == 0 {
c.Data["json"] = Response{Status: "error", Msg: "missing keyword"}
c.ServeJSON()
return
}
topics := object.SearchTopics(keyword)
c.Data["json"] = Response{Status: "ok", Data: topics}
c.ServeJSON()
}

77
controllers/sensitive.go Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"github.com/casbin/casnode/object"
)
// @Title AddSensitive
// @router /add-sensitive [get]
// @Tag Seneistive API
func (c *ApiController) AddSensitive() {
if c.RequireAdmin() {
return
}
sensitiveWord := c.Input().Get("word")
if sensitiveWord == "" {
c.ResponseError("You didn't input a sensitive word.")
return
}
if len(sensitiveWord) > 64 {
c.ResponseError("This sensitive word is too long.")
return
}
if object.IsSensitiveWord(sensitiveWord) {
c.ResponseError("This is already a sensitive word.")
return
}
object.AddSensitiveWord(sensitiveWord)
c.ResponseOk()
}
// @Title DelSensitive
// @router /del-sensitive [get]
// @Tag Seneistive API
func (c *ApiController) DelSensitive() {
if c.RequireAdmin() {
return
}
sensitiveWord := c.Input().Get("word")
if sensitiveWord == "" {
c.ResponseError("You didn't input a sensitive word.")
return
}
if !object.IsSensitiveWord(sensitiveWord) {
c.ResponseError("This is not a sensitive word.")
return
}
object.DeleteSensitiveWord(sensitiveWord)
c.ResponseOk()
}
// @Title GetSensitive
// @router /get-sensitive [get]
// @Tag Seneistive API
func (c *ApiController) GetSensitive() {
c.Data["json"] = object.GetSensitiveWords()
c.ServeJSON()
}

171
controllers/tab.go Normal file
View File

@ -0,0 +1,171 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
// @router /get-tabs [get]
// @Title GetTabs
// @Tag Tab API
func (c *ApiController) GetTabs() {
c.Data["json"] = object.GetHomePageTabs()
c.ServeJSON()
}
// @router /get-all-tabs [get]
// @Title GetAllTabs
// @Tag Tab API
func (c *ApiController) GetAllTabs() {
c.Data["json"] = object.GetAllTabs()
c.ServeJSON()
}
// @router /get-tabs-admin [get]
// @Title GetAllTabsAdmin
// @Tag Tab API
func (c *ApiController) GetAllTabsAdmin() {
c.Data["json"] = object.GetAllTabsAdmin()
c.ServeJSON()
}
// @router /get-tabs-admin [get]
// @Title GetTabAdmin
// @Tag Tab API
func (c *ApiController) GetTabAdmin() {
id := c.Input().Get("id")
c.Data["json"] = object.GetTabAdmin(id)
c.ServeJSON()
}
// @router /get-tabs-admin [get]
// @Title AddTab
// @Tag Tab API
func (c *ApiController) AddTab() {
if c.RequireAdmin() {
return
}
var tabInfo object.AdminTabInfo
var resp Response
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tabInfo)
if err != nil {
panic(err)
}
if tabInfo.Id == "" || tabInfo.Name == "" || tabInfo.Sorter <= 0 {
resp = Response{Status: "fail", Msg: "Some information is missing"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.HasTab(tabInfo.Id) {
resp = Response{Status: "fail", Msg: "Tab ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
tab := object.Tab{
Id: tabInfo.Id,
Name: tabInfo.Name,
Sorter: tabInfo.Sorter,
CreatedTime: util.GetCurrentTime(),
DefaultNode: tabInfo.DefaultNode,
HomePage: tabInfo.HomePage,
}
res := object.AddTab(&tab)
c.ResponseOk(res)
}
// @router /update-tab [post]
// @Title UpdateTab
// @Tag Tab API
func (c *ApiController) UpdateTab() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
var tabInfo object.AdminTabInfo
err := json.Unmarshal(c.Ctx.Input.RequestBody, &tabInfo)
if err != nil {
panic(err)
}
tab := object.Tab{
// Id: tabInfo.Id,
Name: tabInfo.Name,
Sorter: tabInfo.Sorter,
CreatedTime: tabInfo.CreatedTime,
DefaultNode: tabInfo.DefaultNode,
HomePage: tabInfo.HomePage,
}
res := object.UpdateTab(id, &tab)
c.ResponseOk(res)
}
// @router /delete-tab [post]
// @Title DeleteTab
// @Tag Tab API
func (c *ApiController) DeleteTab() {
if c.RequireAdmin() {
return
}
id := c.Input().Get("id")
resp := Response{Status: "ok", Msg: "success", Data: object.DeleteTab(id)}
c.Data["json"] = resp
c.ServeJSON()
}
// @router /get-tab-with-nodes [get]
// @Title GetTabWithNodes
// @Tag Tab API
func (c *ApiController) GetTabWithNodes() {
id := c.Input().Get("id")
if len(id) == 0 {
id = object.GetDefaultTab()
}
tabInfo := object.GetTab(id)
nodes := object.GetNodesByTab(id)
resp := Response{Status: "ok", Msg: "success", Data: tabInfo, Data2: nodes}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) GetTabNodes() {
id := c.Input().Get("id")
if len(id) == 0 {
id = object.GetDefaultTab()
}
c.Data["json"] = object.GetNodesByTab(id)
c.ServeJSON()
}

View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE+TCCAuGgAwIBAgIDAeJAMA0GCSqGSIb3DQEBCwUAMDYxHTAbBgNVBAoTFENh
c2Rvb3IgT3JnYW5pemF0aW9uMRUwEwYDVQQDEwxDYXNkb29yIENlcnQwHhcNMjEx
MDE1MDgxMTUyWhcNNDExMDE1MDgxMTUyWjA2MR0wGwYDVQQKExRDYXNkb29yIE9y
Z2FuaXphdGlvbjEVMBMGA1UEAxMMQ2FzZG9vciBDZXJ0MIICIjANBgkqhkiG9w0B
AQEFAAOCAg8AMIICCgKCAgEAsInpb5E1/ym0f1RfSDSSE8IR7y+lw+RJjI74e5ej
rq4b8zMYk7HeHCyZr/hmNEwEVXnhXu1P0mBeQ5ypp/QGo8vgEmjAETNmzkI1NjOQ
CjCYwUrasO/f/MnI1C0j13vx6mV1kHZjSrKsMhYY1vaxTEP3+VB8Hjg3MHFWrb07
uvFMCJe5W8+0rKErZCKTR8+9VB3janeBz//zQePFVh79bFZate/hLirPK0Go9P1g
OvwIoC1A3sarHTP4Qm/LQRt0rHqZFybdySpyWAQvhNaDFE7mTstRSBb/wUjNCUBD
PTSLVjC04WllSf6Nkfx0Z7KvmbPstSj+btvcqsvRAGtvdsB9h62Kptjs1Yn7GAuo
I3qt/4zoKbiURYxkQJXIvwCQsEftUuk5ew5zuPSlDRLoLByQTLbx0JqLAFNfW3g/
pzSDjgd/60d6HTmvbZni4SmjdyFhXCDb1Kn7N+xTojnfaNkwep2REV+RMc0fx4Gu
hRsnLsmkmUDeyIZ9aBL9oj11YEQfM2JZEq+RVtUx+wB4y8K/tD1bcY+IfnG5rBpw
IDpS262boq4SRSvb3Z7bB0w4ZxvOfJ/1VLoRftjPbLIf0bhfr/AeZMHpIKOXvfz4
yE+hqzi68wdF0VR9xYc/RbSAf7323OsjYnjjEgInUtRohnRgCpjIk/Mt2Kt84Kb0
wn8CAwEAAaMQMA4wDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAn2lf
DKkLX+F1vKRO/5gJ+Plr8P5NKuQkmwH97b8CS2gS1phDyNgIc4/LSdzuf4Awe6ve
C06lVdWSIis8UPUPdjmT2uMPSNjwLxG3QsrimMURNwFlLTfRem/heJe0Zgur9J1M
8haawdSdJjH2RgmFoDeE2r8NVRfhbR8KnCO1ddTJKuS1N0/irHz21W4jt4rxzCvl
2nR42Fybap3O/g2JXMhNNROwZmNjgpsF7XVENCSuFO1jTywLaqjuXCg54IL7XVLG
omKNNNcc8h1FCeKj/nnbGMhodnFWKDTsJcbNmcOPNHo6ixzqMy/Hqc+mWYv7maAG
Jtevs3qgMZ8F9Qzr3HpUc6R3ZYYWDY/xxPisuKftOPZgtH979XC4mdf0WPnOBLqL
2DJ1zaBmjiGJolvb7XNVKcUfDXYw85ZTZQ5b9clI4e+6bmyWqQItlwt+Ati/uFEV
XzCj70B4lALX6xau1kLEpV9O1GERizYRz5P9NJNA7KoO5AVMp9w0DQTkt+LbXnZE
HHnWKy8xHQKZF9sR7YBPGLs/Ac6tviv5Ua15OgJ/8dLRZ/veyFfGo2yZsI+hKVU5
nCCJHBcAyFnm1hdvdwEdH33jDBjNB6ciotJZrf/3VYaIWSalADosHAgMWfXuWP+h
8XKXmzlxuHbTMQYtZPDgspS5aK+S4Q9wb8RRAYo=
-----END CERTIFICATE-----

854
controllers/topic.go Normal file
View File

@ -0,0 +1,854 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
)
type NewTopicForm struct {
Title string `json:"title"`
Body string `json:"body"`
NodeId string `json:"nodeId"`
EditorType string `json:"editorType"`
Tags []string `json:"tags"`
}
// @Title GetTopics
// @Description get current topics
// @Param limit query string true "topics size"
// @Param page query string true "offset"
// @Success 200 {array} object.TopicWithAvatar The Response object
// @router /get-topics [get]
// @Tag Topic API
func (c *ApiController) GetTopics() {
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultHomePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
c.Data["json"] = object.GetTopics(limit, offset)
c.ServeJSON()
}
// @Title GetTopicsAdmin
// @Description get topics for admin
// @Param limit query string true "topics size"
// @Param page query string true "offset"
// @Param un query string true "username(author)"
// @Param ti query string true "search: title"
// @Param cn query string true "search: content"
// @Param sdt query string true "sort: show deleted topics"
// @Param cs query string true "sort: created time"
// @Param lrs query string true "sort: last reply time"
// @Param us query string true "sort: username"
// @Param rcs query string true "sort: reply count"
// @Param hs query string true "sort: hot"
// @Param fcs query string true "sort: favorite count"
// @Success 200 {object} controllers.Response The Response object
// @router /get-topics-admin [get]
// @Tag Topic API
func (c *ApiController) GetTopicsAdmin() {
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
usernameSearchKw := c.Input().Get("un") // search: username(author)
titleSearchKw := c.Input().Get("ti") // search: title
contentSearchKw := c.Input().Get("cn") // search: content
showDeletedTopics := c.Input().Get("sdt") // sort: show deleted topics
createdTimeSort := c.Input().Get("cs") // sort: created time
lastReplySort := c.Input().Get("lrs") // sort: last reply time
usernameSort := c.Input().Get("us") // sort: username
replyCountSort := c.Input().Get("rcs") // sort: reply count
hotSort := c.Input().Get("hs") // sort: hot
favCountSort := c.Input().Get("fcs") // sort: favorite count
defaultLimit := object.DefaultHomePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
res, num := object.GetTopicsAdmin(usernameSearchKw, titleSearchKw, contentSearchKw, showDeletedTopics, createdTimeSort, lastReplySort, usernameSort, replyCountSort, hotSort, favCountSort, limit, offset)
c.Data["json"] = Response{Status: "ok", Msg: "success", Data: res, Data2: num}
c.ServeJSON()
}
// @Title GetTopic
// @Description get one topic by id
// @Param id query string true "id"
// @Success 200 {object} object.TopicWithAvatar The Response object
// @router /get-topic [get]
// @Tag Topic API
func (c *ApiController) GetTopic() {
user := c.GetSessionUser()
id := util.ParseInt(c.Input().Get("id"))
topic := object.GetTopicWithAvatar(id, user)
if topic == nil || topic.Deleted {
c.Data["json"] = nil
c.ServeJSON()
return
}
if user != nil {
topic.NodeModerator = object.CheckNodeModerator(user, topic.NodeId)
}
c.Data["json"] = topic
c.ServeJSON()
}
// @Title GetTopicAdmin
// @Description get topic for admin by id
// @Param id query string true "id"
// @Success 200 {object} object.AdminTopicInfo The Response object
// @router /get-topic-admin [get]
// @Tag Topic API
func (c *ApiController) GetTopicAdmin() {
idStr := c.Input().Get("id")
id := util.ParseInt(idStr)
c.Data["json"] = object.GetTopicAdmin(id)
c.ServeJSON()
}
func (c *ApiController) UpdateTopic() {
idStr := c.Input().Get("id")
var topic object.Topic
err := json.Unmarshal(c.Ctx.Input.RequestBody, &topic)
if err != nil {
panic(err)
}
id := util.ParseInt(idStr)
c.Data["json"] = object.UpdateTopic(id, &topic)
c.ServeJSON()
}
// @Title AddTopic
// @Description add one topic
// @Param form body controllers.NewTopicForm true "topic info"
// @Success 200 {object} controllers.Response The Response object
// @router /add-topic [post]
// @Tag Topic API
func (c *ApiController) AddTopic() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
if object.IsForbidden(user) {
c.ResponseError("Your account has been forbidden to perform this operation")
return
}
var form NewTopicForm
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
}
title, body, nodeId, editorType, tags := form.Title, form.Body, form.NodeId, form.EditorType, form.Tags
node := object.GetNode(nodeId)
if node == nil {
c.ResponseError("Node does not exist.")
return
}
if object.ContainsSensitiveWord(title) {
c.ResponseError("Topic title contains sensitive word.")
return
}
if object.ContainsSensitiveWord(body) {
c.ResponseError("Topic body contains sensitive word.")
return
}
if len(tags) == 0 {
tags = service.Finalword(body)
}
topic := object.Topic{
// Id: util.IntToString(object.GetTopicId()),
Author: GetUserName(user),
NodeId: node.Id,
NodeName: node.Name,
TabId: node.TabId,
Title: title,
CreatedTime: util.GetCurrentTime(),
Tags: tags,
LastReplyUser: "",
LastReplyTime: util.GetCurrentTime(),
UpCount: 0,
DownCount: 0,
HitCount: 0,
FavoriteCount: 0,
SubscribeCount: 0,
Content: body,
Deleted: false,
EditorType: editorType,
IsHidden: node.IsHidden,
}
balance := object.GetMemberBalance(user)
if balance < object.CreateTopicCost {
c.ResponseError("You don't have enough balance.")
return
}
// payRes := object.CreateTopicConsumption(c.GetSessionUser(), topic.Id)
// object.AddTopicNotification(topic.Id, c.GetSessionUser(), body)
err = json.Unmarshal(c.Ctx.Input.RequestBody, &topic)
if err != nil {
panic(err)
}
topics := object.GetTopicsByTitleAndAuthor(topic.Title, topic.Author)
if len(topics) != 0 {
c.ResponseError("Duplicate topic")
return
}
res, id := object.AddTopic(&topic)
if res {
object.CreateTopicConsumption(user, id)
c.UpdateAccountBalance(-object.CreateTopicCost)
c.UpdateAccountConsumptionSum(object.CreateTopicCost)
object.AddTopicNotification(id, topic.Author, topic.Content)
targetNode := object.GetNode(topic.NodeId)
targetNode.AddTopicToMailingList(topic.Title, topic.Content, topic.Author)
c.ResponseOk(topic.Id)
} else {
c.ResponseError("Failed to add topic.")
}
}
// @Title UploadTopicPic
// @Description upload topic picture
// @Param pic formData string true "the picture base64 code"
// @Param type formData string true "the picture type"
// @Success 200 {object} _controllers.Response The Response object
// @router /upload-topic-pic [post]
// @Tag Topic API
func (c *ApiController) UploadTopicPic() {
if c.RequireSignedIn() {
return
}
memberId := c.GetSessionUsername()
fileBase64 := c.Ctx.Request.Form.Get("pic")
fileType := c.Ctx.Request.Form.Get("type")
index := strings.Index(fileBase64, ",")
fileBytes, _ := base64.StdEncoding.DecodeString(fileBase64[index+1:])
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
fileUrl, _ := service.UploadFileToStorage(memberId, "topicPic", "UploadTopicPic", fmt.Sprintf("casnode/topicPic/%s/%s.%s", memberId, timestamp, fileType), fileBytes)
resp := Response{Status: "ok", Msg: timestamp + "." + fileType, Data: fileUrl}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title DeleteTopic
// @Description delete a topic by id
// @Param id query string true "topic id"
// @Success 200 {bool} bool Delete success or failure
// @router /delete-topic [post]
// @Tag Topic API
func (c *ApiController) DeleteTopic() {
idStr := c.Input().Get("id")
user := c.GetSessionUser()
id := util.ParseInt(idStr)
nodeId := object.GetTopicNodeId(id)
if !object.CheckIsAdmin(user) && !object.CheckNodeModerator(user, nodeId) {
resp := Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
c.Data["json"] = object.DeleteTopic(id)
c.ServeJSON()
}
// @Title GetTopicsNum
// @Description get the total number of topics
// @Success 200 {int} int The topic nums
// @router /get-topics-num [get]
// @Tag Topic API
func (c *ApiController) GetTopicsNum() {
c.Data["json"] = object.GetTopicNum()
c.ServeJSON()
}
// @Title GetAllCreatedTopics
// @Description get all created topics
// @Param id query string true "author id"
// @Param tab query string true "tab"
// @Param limit query string true "mumber of topics"
// @Param page query string true "page offset"
// @Success 200 {array} object.Topic The Response object
// @router /get-all-created-topics [get]
// @Tag Topic API
func (c *ApiController) GetAllCreatedTopics() {
author := c.Input().Get("id")
tab := c.Input().Get("tab")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
var (
limit, offset int
err error
)
if len(limitStr) != 0 {
limit, err = strconv.Atoi(limitStr)
if err != nil {
panic(err)
}
} else {
limit = 10
}
if len(pageStr) != 0 {
page, err := strconv.Atoi(pageStr)
if err != nil {
panic(err)
}
offset = page*limit - limit
}
c.Data["json"] = object.GetAllCreatedTopics(author, tab, limit, offset)
c.ServeJSON()
}
// @Title GetCreatedTopicsNum
// @Description get created topics count
// @Param id query string true "member id"
// @Success 200 {int} int topics count
// @router /get-created-topics-num [get]
// @Tag Topic API
func (c *ApiController) GetCreatedTopicsNum() {
memberId := c.Input().Get("id")
c.Data["json"] = object.GetCreatedTopicsNum(memberId)
c.ServeJSON()
}
// @Title GetTopicsByNode
// @Description get topics by node
// @Param node-id query string true "node id"
// @Param limit query string true "number of topics"
// @Param page query string true "page offset"
// @Success 200 {array} object.NodeTopic The Response object
// @router /get-topics-by-node [get]
// @Tag Topic API
func (c *ApiController) GetTopicsByNode() {
nodeId := c.Input().Get("node-id")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultPageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
c.Data["json"] = object.GetTopicsByNode(nodeId, limit, offset)
c.ServeJSON()
}
// @Title GetTopicsByTag
// @Description get topics by tag
// @Param tag-id query string true "tag id"
// @Param limit query string true "number of topics"
// @Param page query string true "page offset"
// @Success 200 {array} object.NodeTopic The Response object
// @router /get-topics-by-tag [get]
// @Tag Topic API
func (c *ApiController) GetTopicsByTag() {
tagId := c.Input().Get("tag-id")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultPageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
c.Data["json"] = object.GetTopicsByTag(tagId, limit, offset)
c.ServeJSON()
}
// @Title AddTopicHitCount
// @Description add topic hit count,together with node
// @Param id query string true "topic id"
// @Success 200 {object} controller.Response The Response object
// @router /add-topic-hit-count [post]
// @Tag Topic API
func (c *ApiController) AddTopicHitCount() {
topicIdStr := c.Input().Get("id")
var resp Response
topicId := util.ParseInt(topicIdStr)
res := object.AddTopicHitCount(topicId)
topicInfo := object.GetTopic(topicId)
hitRecord := object.BrowseRecord{
MemberId: c.GetSessionUsername(),
RecordType: 1,
ObjectId: topicInfo.NodeId,
CreatedTime: util.GetCurrentTime(),
Expired: false,
}
object.AddBrowseRecordNum(&hitRecord)
if res {
resp = Response{Status: "ok", Msg: "success"}
} else {
resp = Response{Status: "fail", Msg: "add topic hit count failed"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title GetTopicsByTab
// @Description get topics by tab
// @Param tab-id query string true "tab id"
// @Param limit query string true "number of topics"
// @Param page query string true "page offset"
// @Success 200 {array} object.TopicWithAvatar The Response object
// @router /get-topics-by-tab [get]
// @Tag Topic API
func (c *ApiController) GetTopicsByTab() {
tabId := c.Input().Get("tab-id")
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
defaultLimit := object.DefaultHomePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
c.Data["json"] = object.GetTopicsWithTab(tabId, limit, offset)
c.ServeJSON()
}
// @Title AddTopicBrowseCount
// @Description add topic browse count
// @Param id query string true "topicId"
// @Success 200 {object} controller.Response The Response object
// @router /add-topic-browse-record [post]
// @Tag Topic API
func (c *ApiController) AddTopicBrowseCount() {
topicId := c.Input().Get("id")
var resp Response
hitRecord := object.BrowseRecord{
MemberId: c.GetSessionUsername(),
RecordType: 2,
ObjectId: topicId,
CreatedTime: util.GetCurrentTime(),
Expired: false,
}
res := object.AddBrowseRecordNum(&hitRecord)
if res {
resp = Response{Status: "ok", Msg: "success"}
} else {
resp = Response{Status: "fail", Msg: "add node hit count failed"}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title GetHotTopic
// @Description get hot topic
// @Param limit query string true "limit size"
// @Success 200 {object} controller.Response The Response object
// @router /get-hot-topic [get]
// @Tag Topic API
func (c *ApiController) GetHotTopic() {
limitStr := c.Input().Get("limit")
defaultLimit := object.HotTopicNum
var limit int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
res := object.GetHotTopic(limit)
c.ResponseOk(res)
}
// @Title GetSortedTopics
// @Description get sorted topics
// @Param lps query string true "sort: last reply count"
// @Param hs query string true "sort: hot"
// @Param fcs query string true "sort: favorite count"
// @Param cts query string true "sort: created time"
// @Param page query string true "offset"
// @Param limit query string true "limit size"
// @Success 200 {object} controller.Response The Response object
// @router /get-hot-topic [get]
// @Tag Topic API
func (c *ApiController) GetSortedTopics() {
limitStr := c.Input().Get("limit")
pageStr := c.Input().Get("page")
lastReplySort := c.Input().Get("lps") // sort: last reply time
hotSort := c.Input().Get("hs") // sort: hot
favCountSort := c.Input().Get("fcs") // sort: favorite count
createdTimeSort := c.Input().Get("cts") // sort: created time
defaultLimit := object.DefaultHomePageNum
var limit, offset int
if len(limitStr) != 0 {
limit = util.ParseInt(limitStr)
} else {
limit = defaultLimit
}
if len(pageStr) != 0 {
page := util.ParseInt(pageStr)
offset = page*limit - limit
}
res := object.GetSortedTopics(lastReplySort, hotSort, favCountSort, createdTimeSort, limit, offset)
c.ResponseOk(res)
}
// @Title UpdateTopicNode
// @Description update the topic node
// @Param updateTopicNode body controllers.updateTopicNode true "topic node info"
// @Success 200 {object} controllers.Response The Response object
// @router /update-topic-node [post]
// @Tag Topic API
func (c *ApiController) UpdateTopicNode() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
var form updateTopicNode
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
}
id, _, nodeId := form.Id, form.NodeName, form.NodeId
originalNode := object.GetTopicNodeId(id)
if !object.CheckIsAdmin(user) && !object.CheckNodeModerator(user, originalNode) && object.GetTopicAuthor(id).Name != GetUserName(user) {
c.ResponseError("Unauthorized.")
return
}
node := object.GetNode(nodeId)
if node == nil {
c.ResponseError("Node does not exist.")
return
}
topic := object.Topic{
// Id: id,
NodeId: node.Id,
NodeName: node.Name,
TabId: node.TabId,
}
res := object.UpdateTopicWithLimitCols(id, &topic)
c.ResponseOk(res)
}
// @Title EditContent
// @Description edit content
// @Param editType query string true "edit Type"
// @Success 200 {object} controllers.Response The Response object
// @router /edit-content [post]
// @Tag Topic API
func (c *ApiController) EditContent() {
if c.RequireSignedIn() {
return
}
user := c.GetSessionUser()
editType := c.Input().Get("editType")
var resp Response
if editType == "topic" {
var form editTopic
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
}
id, title, content, nodeId, editorType, tags := form.Id, form.Title, form.Content, form.NodeId, form.EditorType, form.Tags
if !object.CheckIsAdmin(user) && !object.CheckNodeModerator(user, nodeId) && object.GetTopicAuthor(id).Name != GetUserName(user) {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
topic := object.Topic{
Id: id,
Title: title,
Content: content,
EditorType: editorType,
Tags: tags,
}
res := object.UpdateTopicWithLimitCols(id, &topic)
resp = Response{Status: "ok", Msg: "success", Data: res}
} else {
var form editReply
err := json.Unmarshal(c.Ctx.Input.RequestBody, &form)
if err != nil {
panic(err)
}
id, content, editorType := form.Id, form.Content, form.EditorType
if !object.CheckIsAdmin(user) && object.GetReplyAuthor(id).Name != GetUserName(user) {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
reply := object.Reply{
Id: id,
Content: content,
EditorType: editorType,
}
res := object.UpdateReplyWithLimitCols(id, &reply)
resp = Response{Status: "ok", Msg: "success", Data: res}
}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title TranslateTopic
// @router /translate-topic [get]
// @Tag Topic API
func (c *ApiController) TranslateTopic() {
topicIdStr := c.Input().Get("id")
targetLang := c.Input().Get("target")
// ISO/IEC 15897 to ISO 639-1
targetLang = targetLang[0:2]
topicId := util.ParseInt(topicIdStr)
translateData := &object.TranslateData{}
topic := object.GetTopic(topicId)
if topic == nil || topic.Deleted {
translateData.ErrMsg = "Invalid TopicId"
c.Data["json"] = translateData
c.ServeJSON()
return
}
c.Data["json"] = object.StrTranslate(topic.Content, targetLang)
c.ServeJSON()
return
}
// TopTopic tops topic according to the topType in the url.
// @Title TopTopic
// @Description tops topic according to the topType in the url.
// @Param id query string true "id"
// @Success 200 {object} controllers.Response The Response object
// @router /top-topic [post]
// @Tag Topic API
func (c *ApiController) TopTopic() {
if c.RequireSignedIn() {
return
}
id := util.ParseInt(c.Input().Get("id"))
user := c.GetSessionUser()
var res bool
nodeId := object.GetTopicNodeId(id)
if object.CheckIsAdmin(user) || object.CheckNodeModerator(user, nodeId) {
// timeStr := c.Input().Get("time")
// time := util.ParseInt(timeStr)
// date := util.GetTimeMinute(time)
// res = object.ChangeTopicTopExpiredTime(id, date)
topType := c.Input().Get("topType")
date := util.GetTimeYear(100)
res = object.ChangeTopicTopExpiredTime(id, date, topType)
} else if object.GetTopicAuthor(id).Name == GetUserName(user) {
balance := object.GetMemberBalance(user)
if balance < object.TopTopicCost {
c.ResponseError("You don't have enough balance.")
return
}
object.TopTopicConsumption(user, id)
c.UpdateAccountBalance(-object.TopTopicCost)
date := util.GetTimeMinute(object.DefaultTopTopicTime)
res = object.ChangeTopicTopExpiredTime(id, date, "node")
} else {
c.ResponseError("Unauthorized.")
return
}
c.ResponseOk(res)
}
// @Title CancelTopTopic
// @Description cancels top topic according to the topType in the url.
// @Param id query string true "id"
// @Success 200 {object} controllers.Response The Response object
// @router /cancel-top-topic [post]
// @Tag Topic API
func (c *ApiController) CancelTopTopic() {
if c.RequireSignedIn() {
return
}
idStr := c.Input().Get("id")
user := c.GetSessionUser()
id := util.ParseInt(idStr)
var resp Response
var res bool
nodeId := object.GetTopicNodeId(id)
if object.CheckIsAdmin(user) || object.CheckNodeModerator(user, nodeId) {
topType := c.Input().Get("topType")
res = object.ChangeTopicTopExpiredTime(id, "", topType)
} else {
resp = Response{Status: "fail", Msg: "Unauthorized."}
c.Data["json"] = resp
c.ServeJSON()
return
}
resp = Response{Status: "ok", Msg: "success", Data: res}
c.Data["json"] = resp
c.ServeJSON()
}
// @Title GetTopicByUrlPathAndTitle
// @router /get-topic-by-urlpath-and-title [get]
// @Tag Topic API
func (c *ApiController) GetTopicByUrlPathAndTitle() {
urlPath := c.Input().Get("urlPath")
title := c.Input().Get("title")
author := c.Input().Get("author")
nodeId := c.Input().Get("nodeId")
if urlPath == "" {
c.ResponseError(fmt.Sprintf("The urlPath: %s does not exist", urlPath))
return
}
if title == "" {
c.ResponseError(fmt.Sprintf("The title: %s does not exist", title))
return
}
if author == "" {
c.ResponseError(fmt.Sprintf("The author: %s does not exist", author))
return
}
node := object.GetNode(nodeId)
if nodeId == "" || node == nil {
c.ResponseError(fmt.Sprintf("The node: %s does not exist", nodeId))
return
}
topic := object.GetTopicByUrlPathAndTitle(urlPath, title, nodeId)
if topic == nil {
topic = &object.Topic{
Author: author,
NodeId: nodeId,
NodeName: node.Name,
TabId: node.TabId,
Title: title,
CreatedTime: util.GetCurrentTime(),
LastReplyTime: util.GetCurrentTime(),
Content: fmt.Sprintf("URL: %s%s", nodeId, urlPath),
UrlPath: urlPath,
EditorType: "markdown",
IsHidden: node.IsHidden,
}
object.AddTopic(topic)
}
c.ResponseOk(topic)
}

105
controllers/translator.go Normal file
View File

@ -0,0 +1,105 @@
package controllers
import (
"encoding/json"
"github.com/casbin/casnode/object"
)
// @router /update-translator [post]
// @Title UpdateTranslator
// @Tag Translator API
func (c *ApiController) UpdateTranslator() {
if c.RequireAdmin() {
return
}
var translator object.Translator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &translator)
if err != nil {
panic(err)
}
object.UpdateTranslator(translator)
c.ResponseOk()
}
// @router /add-translator [post]
// @Title AddTranslator
// @Tag Translator API
func (c *ApiController) AddTranslator() {
if c.RequireAdmin() {
return
}
var resp Response
var translator object.Translator
err := json.Unmarshal(c.Ctx.Input.RequestBody, &translator)
if err != nil {
panic(err)
}
res := object.GetTranslator(translator.Id)
if len(*res) > 0 {
resp = Response{Status: "fail", Msg: "Translator ID existed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
if object.AddTranslator(translator) {
c.ResponseOk()
return
} else {
resp = Response{Status: "fail", Msg: "Add translator failed"}
c.Data["json"] = resp
c.ServeJSON()
return
}
}
// @router /get-translator [get]
// @Title GetTranslator
// @Tag Translator API
func (c *ApiController) GetTranslator() {
id := c.Input().Get("id")
res := object.GetTranslator(id)
c.Data["json"] = res
c.ServeJSON()
}
func (c *ApiController) GetEnableTranslator() {
res := object.GetEnableTranslator()
c.Data["json"] = res
c.ServeJSON()
}
// @router /visible-translator [get]
// @Title VisibleTranslator
// @Tag Translator API
func (c *ApiController) VisibleTranslator() {
c.ResponseOk(false)
res := object.GetEnableTranslator()
if res != nil {
if res.Visible {
c.ResponseOk(true)
}
}
}
// @router /del-translator [post]
// @Title DelTranslator
// @Tag Translator API
func (c *ApiController) DelTranslator() {
id := c.Input().Get("id")
resp := Response{Status: "fail", Msg: "Delete translator failed"}
if object.DelTranslator(id) {
resp = Response{Status: "ok", Msg: "Success"}
}
c.Data["json"] = resp
c.ServeJSON()
}

86
controllers/type.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
import "github.com/casbin/casnode/object"
type GetAccessTokenRespFromWeChat struct {
AccessToken string `json:"access_token"`
ExpiresIn float64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Openid string `json:"openid"`
Scope string `json:"scope"`
}
type authResponse struct {
IsAuthenticated bool `json:"isAuthenticated"`
IsSignedUp bool `json:"isSignedUp"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Addition string `json:"addition"`
}
type newNotification struct {
ObjectId int `json:"objectId"`
NotificationType int `json:"notificationType"`
ReceiverId string `json:"receiverId"`
}
type updateTopicNode struct {
Id int `json:"id"`
NodeId string `json:"nodeId"`
NodeName string `json:"nodeName"`
}
type editTopic struct {
Id int `json:"id"`
Title string `json:"title"`
NodeId string `json:"nodeId"`
Content string `json:"content"`
Tags []string `json:"tags"`
EditorType string `json:"editorType"`
}
type editReply struct {
Id int `json:"id"`
Content string `json:"content"`
EditorType string `json:"editorType"`
}
type fileDescribe struct {
Desc string `json:"desc"`
FileName string `json:"fileName"`
}
type fileNumResp struct {
Num int `json:"num"`
MaxNum int `json:"maxNum"`
}
type addNodeModerator struct {
NodeId string `json:"nodeId"`
MemberId string `json:"memberId"`
}
type deleteNodeModerator struct {
NodeId string `json:"nodeId"`
MemberId string `json:"memberId"`
}
type adminNodeInfo struct {
NodeInfo object.Node `json:"nodeInfo"`
TopicNum int `json:"topicNum"`
FavoritesNum int `json:"favoritesNum"`
}

67
controllers/util.go Normal file
View File

@ -0,0 +1,67 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package controllers
type Response struct {
Status string `json:"status"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
Data2 interface{} `json:"data2"`
}
func (c *ApiController) ResponseOk(data ...interface{}) {
resp := Response{Status: "ok"}
switch len(data) {
case 2:
resp.Data2 = data[1]
fallthrough
case 1:
resp.Data = data[0]
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) ResponseError(error string, data ...interface{}) {
resp := Response{Status: "error", Msg: error}
switch len(data) {
case 2:
resp.Data2 = data[1]
fallthrough
case 1:
resp.Data = data[0]
}
c.Data["json"] = resp
c.ServeJSON()
}
func (c *ApiController) RequireSignedIn() bool {
if c.GetSessionUser() == nil {
c.ResponseError("please sign in first")
return true
}
return false
}
func (c *ApiController) RequireAdmin() bool {
user := c.GetSessionUser()
if user == nil || !user.IsAdmin {
c.ResponseError("this operation requires admin privilege")
return true
}
return false
}

589032
dictionary/dictionary.txt Normal file

File diff suppressed because it is too large Load Diff

75
discuzx/adapter.go Normal file
View File

@ -0,0 +1,75 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"runtime"
"github.com/astaxie/beego"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
var adapter *Adapter
func InitAdapter() {
adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), dbName)
}
// Adapter represents the MySQL adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
dbName string
Engine *xorm.Engine
}
// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
err := a.Engine.Close()
if err != nil {
panic(err)
}
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
a.dbName = dbName
// Open the DB, create it if not existed.
a.open()
// Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer)
return a
}
func (a *Adapter) open() {
engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName)
if err != nil {
panic(err)
}
a.Engine = engine
}
func (a *Adapter) close() {
a.Engine.Close()
a.Engine = nil
}

104
discuzx/attachment.go Normal file
View File

@ -0,0 +1,104 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"net/url"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/service"
)
type Attachment struct {
Tid int
Pid int
Uid int
Dateline int
Filename string
Filesize int
Attachment string
Description string
Isimage int
Width int
}
func getAttachmentsInTable(tableIndex int) []*Attachment {
attachments := []*Attachment{}
tableName := fmt.Sprintf("pre_forum_attachment_%d", tableIndex)
err := adapter.Engine.Table(tableName).Find(&attachments)
if err != nil {
panic(err)
}
return attachments
}
func getAttachments() []*Attachment {
attachments := []*Attachment{}
for i := 0; i < 10; i++ {
tmp := getAttachmentsInTable(i)
attachments = append(attachments, tmp...)
}
return attachments
}
func getAttachmentMap() map[int][]*Attachment {
attachments := getAttachments()
m := map[int][]*Attachment{}
for _, attachment := range attachments {
if _, ok := m[attachment.Tid]; !ok {
m[attachment.Tid] = []*Attachment{}
}
m[attachment.Tid] = append(m[attachment.Tid], attachment)
}
return m
}
func uploadDiscuzxFile(username string, fileBytes []byte, fileName string, createdTime string, description string) string {
username = url.QueryEscape(username)
memberId := fmt.Sprintf("%s/%s", CasdoorOrganization, username)
fileUrl, _ := service.UploadFileToStorageSafe(memberId, "file", "uploadDiscuzxFile", fmt.Sprintf("file/%s/%s", memberId, fileName), fileBytes, createdTime, description)
return fileUrl
}
func getRecordFromAttachment(attachment *Attachment, post *Post) *object.UploadFileRecord {
oldFileUrl := fmt.Sprintf("%s%s", discuzxAttachmentBaseUrl, attachment.Attachment)
fileBytes, _, err := downloadFileSafe(oldFileUrl)
if err != nil {
if urlError, ok := err.(*url.Error); ok {
fmt.Printf("\t\t[%d]: getRecordFromAttachment() error: %s, the attachement is deleted: %s\n", post.Pid, urlError.Error(), attachment.Attachment)
return nil
} else {
panic(err)
}
}
fileUrl := uploadDiscuzxFile(post.Author, fileBytes, attachment.Filename, getTimeFromUnixSeconds(attachment.Dateline), attachment.Description)
fileType := "file"
if attachment.Isimage == 1 {
fileType = "image"
}
record := &object.UploadFileRecord{
FileName: attachment.Filename,
FileUrl: fileUrl,
FileType: fileType,
}
return record
}

139
discuzx/avatar.go Normal file
View File

@ -0,0 +1,139 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"crypto/x509"
"fmt"
"net/url"
"path"
"github.com/casbin/casnode/casdoor"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
var discuzxDefaultAvatarUrl string
func init() {
discuzxDefaultAvatarUrl = fmt.Sprintf("%suc_server/images/noavatar_middle.gif", discuzxDomain)
}
func syncAvatarForUser(user *casdoorsdk.User) string {
uid := user.Ranking
username := user.Name
oldAvatarUrl := fmt.Sprintf("%suc_server/avatar.php?uid=%d", discuzxDomain, uid)
newAvatarUrl := getRedirectUrl(oldAvatarUrl)
if oldAvatarUrl == newAvatarUrl || newAvatarUrl == "" {
panic(fmt.Errorf("getRedirectUrl() error: oldAvatarUrl == newAvatarUrl, oldAvatarUrl = %s, newAvatarUrl = %s", oldAvatarUrl, newAvatarUrl))
}
var fileBytes []byte
var fileExt string
if newAvatarUrl == discuzxDefaultAvatarUrl {
randomAvatarUrl := getRandomAvatarUrl(username)
fileBytes = getRandomAvatar(randomAvatarUrl)
fileExt = ".png"
user.IsDefaultAvatar = true
go casdoor.UpdateUser(user.Owner, user.Name, user)
} else {
var err error
times := 0
for {
fileBytes, _, err = downloadFile(newAvatarUrl)
if err != nil {
if urlError, ok := err.(*url.Error); ok {
if hostnameError, ok := urlError.Err.(x509.HostnameError); ok {
times += 1
fmt.Printf("[%d]: downloadFile() error: %s, times = %d, use random avatar\n", uid, hostnameError.Error(), times)
if times >= 10 {
panic(err)
}
randomAvatarUrl := getRandomAvatarUrl(username)
fileBytes = getRandomAvatar(randomAvatarUrl)
fileExt = ".png"
user.IsDefaultAvatar = true
go casdoor.UpdateUser(user.Owner, user.Name, user)
break
}
}
times += 1
fmt.Printf("[%d]: downloadFile() error: %s, times = %d\n", uid, err.Error(), times)
if times >= 10 {
panic(err)
}
} else {
break
}
}
if fileExt == "" {
fileExt = path.Ext(newAvatarUrl)
}
if fileExt != ".png" {
fileBytes, fileExt, err = convertImageToPng(fileBytes)
if err != nil {
panic(err)
}
}
}
avatarUrl, err := uploadDiscuzxAvatar(username, fileBytes, fileExt)
if err != nil {
panic(err)
}
return avatarUrl
}
func updateDefaultAvatarForUser(user *casdoorsdk.User) string {
uid := user.Ranking
username := user.Name
defaultAvatarUrl := getRandomAvatarUrl(username)
var fileBytes []byte
var newUrl string
var fileExt string
var err error
times := 0
for {
fileBytes, newUrl, err = downloadFile(defaultAvatarUrl)
if err != nil {
times += 1
fmt.Printf("[%d]: downloadFile() error: %s, times = %d\n", uid, err.Error(), times)
if times >= 10 {
panic(err)
}
} else {
break
}
}
fileExt = path.Ext(newUrl)
avatarUrl, err := uploadDiscuzxAvatar(username, fileBytes, fileExt)
if err != nil {
panic(err)
}
return avatarUrl
}

View File

@ -0,0 +1,57 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"testing"
"github.com/casbin/casnode/casdoor"
"github.com/casbin/casnode/controllers"
"github.com/casbin/casnode/object"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
func TestAvatar(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
casdoor.InitCasdoorAdapter()
url := "https://casbin.org/img/casbin.svg"
downloadFile(url)
}
func TestUpdateDefaultAvatars(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
casdoor.InitCasdoorAdapter()
controllers.InitAuthConfig()
users := casdoor.GetUsers()
sem := make(chan int, SyncAvatarsConcurrency)
for i, user := range users {
sem <- 1
go func(i int, user *casdoorsdk.User) {
if user.IsDefaultAvatar {
avatarUrl := updateDefaultAvatarForUser(user)
fmt.Printf("[%d/%d]: Updated default avatar for user: [%d, %s] as URL: %s\n", i+1, len(users), user.Ranking, user.Name, avatarUrl)
}
<-sem
}(i, user)
}
}

50
discuzx/avatar_test.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"testing"
"github.com/casbin/casnode/casdoor"
"github.com/casbin/casnode/controllers"
"github.com/casbin/casnode/object"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
var SyncAvatarsConcurrency = 20
func TestSyncAvatars(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
casdoor.InitCasdoorAdapter()
controllers.InitAuthConfig()
initRandomAvatars()
users := casdoor.GetUsers()
sem := make(chan int, SyncAvatarsConcurrency)
for i, user := range users {
sem <- 1
go func(i int, user *casdoorsdk.User) {
if user.Avatar == "" {
avatarUrl := syncAvatarForUser(user)
fmt.Printf("[%d/%d]: Synced avatar for user: [%d, %s] as URL: %s\n", i+1, len(users), user.Ranking, user.Name, avatarUrl)
}
<-sem
}(i, user)
}
}

74
discuzx/avatar_util.go Normal file
View File

@ -0,0 +1,74 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"bytes"
"fmt"
"image/gif"
"image/jpeg"
"image/png"
"net/http"
"net/url"
"github.com/casbin/casnode/service"
)
func uploadDiscuzxAvatar(username string, fileBytes []byte, fileExt string) (string, error) {
username = url.QueryEscape(username)
memberId := fmt.Sprintf("%s/%s", CasdoorOrganization, username)
fileUrl, err := service.UploadFileToStorageSafe(memberId, "avatar", "uploadDiscuzxAvatar", fmt.Sprintf("avatar/%s%s", memberId, fileExt), fileBytes, "", "")
return fileUrl, err
}
func convertImageToPng(imageBytes []byte) ([]byte, string, error) {
// Converting jpeg images to png with Golang
// https://medium.com/@daetam/converting-jpeg-to-png-with-golang-85905105cf47
contentType := http.DetectContentType(imageBytes)
switch contentType {
case "image/png":
return imageBytes, ".png", nil
case "image/jpeg":
img, err := jpeg.Decode(bytes.NewReader(imageBytes))
if err != nil {
return nil, "", err
}
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
if err != nil {
return nil, "", err
}
return buf.Bytes(), ".png", nil
case "image/gif":
img, err := gif.Decode(bytes.NewReader(imageBytes))
if err != nil {
return nil, "", err
}
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
if err != nil {
return nil, "", err
}
return buf.Bytes(), ".png", nil
default:
return nil, "", fmt.Errorf("convertImageToPng() error: unsupported contentType: %s", contentType)
}
}

64
discuzx/avatat_cached.go Normal file
View File

@ -0,0 +1,64 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"sync"
)
var randomAvatarCount = 244
var (
randomAvatarMap map[string][]byte
randomAvatarMapMutex sync.RWMutex
)
func initRandomAvatars() {
randomAvatarMap = map[string][]byte{}
randomAvatarMapMutex = sync.RWMutex{}
var wg sync.WaitGroup
wg.Add(randomAvatarCount)
sem := make(chan int, 100)
for i := 1; i <= randomAvatarCount; i++ {
sem <- 1
go func(i int) {
defer wg.Done()
avatarUrl := fmt.Sprintf("%s%d.png", avatarPoolBaseUrl, i)
fileBytes, _, err := downloadFile(avatarUrl)
if err != nil {
panic(err)
}
randomAvatarMapMutex.Lock()
randomAvatarMap[avatarUrl] = fileBytes
randomAvatarMapMutex.Unlock()
fmt.Printf("[%d/%d]: Initialized random avatar: %s\n", i, randomAvatarCount, avatarUrl)
<-sem
}(i)
}
wg.Wait()
}
func getRandomAvatar(avatarUrl string) []byte {
fileBytes, ok := randomAvatarMap[avatarUrl]
if !ok {
panic(fmt.Sprintf("getRandomAvatar() error, key not found: %s", avatarUrl))
}
return fileBytes
}

32
discuzx/casdoor_init.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"github.com/astaxie/beego"
"github.com/casbin/casnode/object"
)
var (
CasdoorOrganization = ""
CasdoorApplication = ""
)
func init() {
object.InitConfig()
CasdoorOrganization = beego.AppConfig.String("casdoorOrganization")
CasdoorApplication = beego.AppConfig.String("casdoorApplication")
}

56
discuzx/class.go Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type Class struct {
Typeid int
Fid int
Name string
Displayorder int
}
func getClasses() []*Class {
classes := []*Class{}
err := adapter.Engine.Table("pre_forum_threadclass").Find(&classes)
if err != nil {
panic(err)
}
return classes
}
func getClass(id int) *Class {
class := Class{Typeid: id}
existed, err := adapter.Engine.Table("pre_forum_threadclass").Get(&class)
if err != nil {
panic(err)
}
if existed {
return &class
} else {
return nil
}
}
func getClassMap() map[int]*Class {
classes := getClasses()
m := map[int]*Class{}
for _, class := range classes {
m[class.Typeid] = class
}
return m
}

23
discuzx/conf.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
var (
dbName = "ultrax"
discuzxDomain = "https://www.discuz.net/"
discuzxAttachmentBaseUrl = "https://attachment.discuz.net/forum/"
)
var avatarPoolBaseUrl = "https://cdn.casbin.com/avatar-pool/"

102
discuzx/forum.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import "fmt"
type Forum struct {
Fid int
Fup int
Type string
Name string
Status int
Displayorder int
Threads int
Forums []*Forum `xorm:"-"`
Parent *Forum `xorm:"-"`
}
func getForums() []*Forum {
forums := []*Forum{}
err := adapter.Engine.Table("pre_forum_forum").Find(&forums)
if err != nil {
panic(err)
}
return forums
}
func getForum(id int) *Forum {
forum := Forum{Fid: id}
existed, err := adapter.Engine.Table("pre_forum_forum").Get(&forum)
if err != nil {
panic(err)
}
if existed {
return &forum
} else {
return nil
}
}
func getForumMap() map[int]*Forum {
forums := getForums()
m := map[int]*Forum{}
for _, forum := range forums {
m[forum.Fid] = forum
}
return m
}
func getForumTree() ([]*Forum, map[int]*Forum) {
res := []*Forum{}
forumMap := getForumMap()
for _, forum := range forumMap {
if forum.Type == "group" {
res = append(res, forum)
} else {
parentForum := forumMap[forum.Fup]
parentForum.Forums = append(parentForum.Forums, forum)
forum.Parent = parentForum
}
}
forumNameCountMap := map[string]int{}
for _, forum := range forumMap {
if forum.Type == "group" {
forumNameCountMap[forum.Name] = 0
continue
}
if v, ok := forumNameCountMap[forum.Name]; ok {
forumNameCountMap[forum.Name] = v + 1
} else {
forumNameCountMap[forum.Name] = 1
}
}
for _, forum := range forumMap {
if forumNameCountMap[forum.Name] > 1 {
parentForum := forumMap[forum.Fup]
forum.Name = fmt.Sprintf("%s-%s", parentForum.Name, forum.Name)
}
}
return res, forumMap
}

57
discuzx/forum_field.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type Field struct {
Fid int
Description string
Icon string
Moderators string
Rules string
}
func getFields() []*Field {
fields := []*Field{}
err := adapter.Engine.Table("pre_forum_forumfield").Find(&fields)
if err != nil {
panic(err)
}
return fields
}
func getField(id int) *Field {
field := Field{Fid: id}
existed, err := adapter.Engine.Table("pre_forum_forumfield").Get(&field)
if err != nil {
panic(err)
}
if existed {
return &field
} else {
return nil
}
}
func getFieldMap() map[int]*Field {
fields := getFields()
m := map[int]*Field{}
for _, field := range fields {
m[field.Fid] = field
}
return m
}

142
discuzx/forum_sync.go Normal file
View File

@ -0,0 +1,142 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"strings"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/util"
)
func getInfoFromField(field *Field) (string, string, []string) {
if field == nil {
return "", "", []string{}
}
desc := field.Description
extra := field.Rules
moderators := []string{}
if field.Moderators != "" {
moderators = strings.Split(field.Moderators, "\t")
}
return desc, extra, moderators
}
func addForums() {
tabs := []*object.Tab{}
nodes := []*object.Node{}
forumTree, _ := getForumTree()
forumFieldMap := getFieldMap()
for i, groupForum := range forumTree {
defaultNode := ""
if len(groupForum.Forums) != 0 {
defaultNode = groupForum.Forums[0].Name
}
field := forumFieldMap[groupForum.Fid]
desc, extra, moderators := getInfoFromField(field)
tab := &object.Tab{
Id: groupForum.Name,
Name: groupForum.Name,
Sorter: groupForum.Displayorder,
Ranking: groupForum.Fid,
CreatedTime: util.GetCurrentTime(),
DefaultNode: defaultNode,
HomePage: true,
Desc: desc,
Extra: extra,
Moderators: moderators,
}
tabs = append(tabs, tab)
fmt.Printf("[%d/%d]: Synced group forum: %s\n", i+1, len(forumTree), groupForum.Name)
for j, forum := range groupForum.Forums {
field2 := forumFieldMap[forum.Fid]
desc2, extra2, moderators2 := getInfoFromField(field2)
forumNode := &object.Node{
Id: forum.Name,
Name: forum.Name,
CreatedTime: util.GetCurrentTime(),
Desc: desc2,
Extra: extra2,
Image: "https://cdn.v2ex.com/navatar/3b8a/6142/215_xxlarge.png?m=1523190513",
TabId: groupForum.Name,
ParentNode: "",
PlaneId: "",
Sorter: forum.Displayorder,
Ranking: forum.Fid,
Hot: forum.Threads,
Moderators: moderators2,
MailingList: "",
GoogleGroupCookie: "",
IsHidden: forum.Status == 0,
}
nodes = append(nodes, forumNode)
fmt.Printf("\t[%d/%d]: Synced forum: %s\n", j+1, len(groupForum.Forums), forum.Name)
for k, subForum := range forum.Forums {
field3 := forumFieldMap[subForum.Fid]
desc3, extra3, moderators3 := getInfoFromField(field3)
subForumNode := &object.Node{
Id: subForum.Name,
Name: subForum.Name,
CreatedTime: util.GetCurrentTime(),
Desc: desc3,
Extra: extra3,
Image: "https://cdn.v2ex.com/navatar/3b8a/6142/215_xxlarge.png?m=1523190513",
TabId: groupForum.Name,
ParentNode: forum.Name,
PlaneId: "",
Sorter: subForum.Displayorder,
Ranking: subForum.Fid,
Hot: subForum.Threads,
Moderators: moderators3,
MailingList: "",
GoogleGroupCookie: "",
IsHidden: subForum.Status == 0,
}
nodes = append(nodes, subForumNode)
fmt.Printf("\t\t[%d/%d]: Synced sub forum: %s\n", k+1, len(forum.Forums), subForum.Name)
}
}
}
defaultNode := ""
if len(nodes) > 0 {
defaultNode = nodes[0].Id
}
tab := &object.Tab{
Id: "all",
Name: "全部",
Sorter: 100,
CreatedTime: util.GetCurrentTime(),
DefaultNode: defaultNode,
HomePage: true,
}
tabs = append(tabs, tab)
object.AddTabs(tabs)
object.AddNodes(nodes)
}

29
discuzx/forum_test.go Normal file
View File

@ -0,0 +1,29 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"testing"
"github.com/casbin/casnode/object"
)
func TestAddForums(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
addForums()
}

67
discuzx/member.go Normal file
View File

@ -0,0 +1,67 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type Member struct {
Uid int
Email string
Username string
Password string
Status int
Emailstatus int
Avatarstatus int
Videophotostatus int
Adminid int
Groupid int
Groupexpiry int
Extgroupids string
Regdate int
Credits int
Allowadmincp int
}
func getMembers() []*Member {
members := []*Member{}
err := adapter.Engine.Table("pre_common_member").Find(&members)
if err != nil {
panic(err)
}
return members
}
func getMember(id int) *Member {
member := Member{Uid: id}
existed, err := adapter.Engine.Table("pre_common_member").Get(&member)
if err != nil {
panic(err)
}
if existed {
return &member
} else {
return nil
}
}
func getMemberMap() map[int]*Member {
members := getMembers()
m := map[int]*Member{}
for _, member := range members {
m[member.Uid] = member
}
return m
}

59
discuzx/member_ex.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type MemberEx struct {
*Member
*Profile
*UcenterMember
}
func getMembersEx() []*MemberEx {
members := getMembers()
profileMap := getProfileMap()
ucenterMemberMap := getUcenterMemberMap()
membersEx := []*MemberEx{}
for _, member := range members {
memberEx := &MemberEx{
Member: member,
Profile: profileMap[member.Uid],
UcenterMember: ucenterMemberMap[member.Uid],
}
membersEx = append(membersEx, memberEx)
}
return membersEx
}
func getMemberEx(id int) *MemberEx {
member := getMember(id)
profile := getProfile(id)
ucenterMember := getUcenterMember(id)
return &MemberEx{
Member: member,
Profile: profile,
UcenterMember: ucenterMember,
}
}
func getMemberExMap() map[int]*MemberEx {
membersEx := getMembersEx()
m := map[int]*MemberEx{}
for _, memberEx := range membersEx {
m[memberEx.Member.Uid] = memberEx
}
return m
}

72
discuzx/member_profile.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type Profile struct {
Uid int
Realname string
Gender int
Birthyear int
Birthmonth int
Birthday int
Mobile string
Idcardtype string
Idcard string
Address string
Resideprovince string
Residecity string
Residedist string
Residecommunity string
Education string
Occupation string
Position string
Site string
Bio string
Interest string
}
func getProfiles() []*Profile {
profiles := []*Profile{}
err := adapter.Engine.Table("pre_common_member_profile").Find(&profiles)
if err != nil {
panic(err)
}
return profiles
}
func getProfile(id int) *Profile {
profile := Profile{Uid: id}
existed, err := adapter.Engine.Table("pre_common_member_profile").Get(&profile)
if err != nil {
panic(err)
}
if existed {
return &profile
} else {
return nil
}
}
func getProfileMap() map[int]*Profile {
profiles := getProfiles()
m := map[int]*Profile{}
for _, profile := range profiles {
m[profile.Uid] = profile
}
return m
}

61
discuzx/member_ucenter.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
type UcenterMember struct {
Uid int
Username string
Password string
Email string
Regip string
Regdate int
Lastloginip int
Lastlogintime int
Salt string
}
func getUcenterMembers() []*UcenterMember {
ucenterMembers := []*UcenterMember{}
err := adapter.Engine.Table("pre_ucenter_members").Find(&ucenterMembers)
if err != nil {
panic(err)
}
return ucenterMembers
}
func getUcenterMember(id int) *UcenterMember {
ucenterMember := UcenterMember{Uid: id}
existed, err := adapter.Engine.Table("pre_ucenter_members").Get(&ucenterMember)
if err != nil {
panic(err)
}
if existed {
return &ucenterMember
} else {
return nil
}
}
func getUcenterMemberMap() map[int]*UcenterMember {
ucenterMembers := getUcenterMembers()
m := map[int]*UcenterMember{}
for _, ucenterMember := range ucenterMembers {
m[ucenterMember.Uid] = ucenterMember
}
return m
}

65
discuzx/post.go Normal file
View File

@ -0,0 +1,65 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import "github.com/casbin/casnode/object"
type Post struct {
Pid int
Tid int
First int
Author string
Subject string
Dateline int
Message string
Useip string
Invisible int
UploadFileRecords []*object.UploadFileRecord `xorm:"-"`
}
func getPosts() []*Post {
posts := []*Post{}
err := adapter.Engine.Table("pre_forum_post").Find(&posts)
// err := adapter.Engine.Table("pre_forum_post").Where("tid = ?", threadId).Find(&posts)
if err != nil {
panic(err)
}
return posts
}
func getThreadPostsMap() (map[int][]*Post, int) {
threadPostsMap := map[int][]*Post{}
posts := getPosts()
for _, post := range posts {
tid := post.Tid
if _, ok := threadPostsMap[tid]; !ok {
threadPostsMap[tid] = []*Post{}
}
threadPostsMap[tid] = append(threadPostsMap[tid], post)
}
return threadPostsMap, len(posts)
}
func getPostMapFromPosts(posts []*Post) map[int]*Post {
res := map[int]*Post{}
for _, post := range posts {
res[post.Pid] = post
}
return res
}

112
discuzx/thread.go Normal file
View File

@ -0,0 +1,112 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"sync"
"github.com/casbin/casnode/object"
)
type Thread struct {
Tid int
Fid int
Typeid int
Author string
Subject string
Dateline int
Lastpost int
Lastposter string
Views int
Replies int
Displayorder int
RecommendAdd int
RecommendSub int
Heats int
Favtimes int
Posts []*Post `xorm:"-"`
}
func getThreads() []*Thread {
threads := []*Thread{}
err := adapter.Engine.Table("pre_forum_thread").Find(&threads)
if err != nil {
panic(err)
}
return threads
}
func getThread(id int) *Thread {
thread := Thread{Tid: id}
existed, err := adapter.Engine.Table("pre_forum_thread").Get(&thread)
if err != nil {
panic(err)
}
if existed {
return &thread
} else {
return nil
}
}
func getThreadMap() map[int]*Thread {
threads := getThreads()
m := map[int]*Thread{}
for _, thread := range threads {
m[thread.Tid] = thread
}
return m
}
func addThread(thread *Thread, threadPostsMap map[int][]*Post, attachments []*Attachment, forum *Forum, classMap map[int]*Class) (*object.Topic, []*object.Reply) {
posts := threadPostsMap[thread.Tid]
postMap := getPostMapFromPosts(posts)
thread.Posts = posts
// deleteWholeTopic(thread)
mutex := sync.RWMutex{}
var wg sync.WaitGroup
wg.Add(len(attachments))
for _, attachment := range attachments {
go func(attachment *Attachment) {
defer wg.Done()
post := postMap[attachment.Pid]
if post != nil {
record := getRecordFromAttachment(attachment, post)
if record != nil {
mutex.Lock()
if post.UploadFileRecords == nil {
post.UploadFileRecords = []*object.UploadFileRecord{}
}
post.UploadFileRecords = append(post.UploadFileRecords, record)
mutex.Unlock()
}
}
}(attachment)
}
wg.Wait()
topic, replies := getTopicAndReplies(thread, forum, classMap)
return topic, replies
}

View File

@ -0,0 +1,46 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"testing"
"github.com/casbin/casnode/object"
)
func TestGetThreads(t *testing.T) {
InitAdapter()
object.InitAdapter()
threadMap := getThreadMap()
for _, thread := range threadMap {
thread.Posts = []*Post{}
}
posts := getPosts()
for _, post := range posts {
if thread, ok := threadMap[post.Tid]; ok {
thread.Posts = append(thread.Posts, post)
} else {
// fmt.Printf("Failed to find thread: %d for post: %s\n", post.Tid, post.Message)
}
}
// thread := threadMap[126152]
thread := threadMap[126239]
println(thread)
getTopicAndReplies(thread, nil, nil)
}

95
discuzx/thread_test.go Normal file
View File

@ -0,0 +1,95 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"sync"
"testing"
"github.com/casbin/casnode/controllers"
"github.com/casbin/casnode/object"
)
var (
AddThreadsConcurrency = 100
AddThreadsBatchSize = 10000
)
func addThreads(threads []*Thread, threadPostsMap map[int][]*Post, attachmentMap map[int][]*Attachment, forumMap map[int]*Forum, classMap map[int]*Class) {
arrayMutex := sync.RWMutex{}
var wg sync.WaitGroup
wg.Add(len(threads))
sem := make(chan int, AddThreadsConcurrency)
topics := []*object.Topic{}
replies := []*object.Reply{}
for i, thread := range threads {
sem <- 1
go func(i int, thread *Thread) {
defer wg.Done()
attachments := attachmentMap[thread.Tid]
forum := forumMap[thread.Fid]
topic, replies2 := addThread(thread, threadPostsMap, attachments, forum, classMap)
if topic != nil && replies2 != nil {
arrayMutex.Lock()
topics = append(topics, topic)
replies = append(replies, replies2...)
arrayMutex.Unlock()
fmt.Printf("\t[%d/%d]: Added thread: tid = %d, fid = %d, replies = %d\n", i+1, len(threads), thread.Tid, thread.Fid, len(replies2))
} else {
fmt.Printf("\t[%d/%d]: Added thread: tid = %d, fid = %d, empty thread, removed\n", i+1, len(threads), thread.Tid, thread.Fid)
}
<-sem
}(i, thread)
}
wg.Wait()
object.AddTopicsInBatch(topics)
object.AddRepliesInBatch(replies)
}
func TestAddThreads(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
controllers.InitAuthConfig()
attachmentMap := getAttachmentMap()
fmt.Printf("Loaded attachments: %d\n", len(attachmentMap))
_, forumMap := getForumTree()
fmt.Printf("Loaded forums: %d\n", len(forumMap))
classMap := getClassMap()
fmt.Printf("Loaded classes: %d\n", len(classMap))
threads := getThreads()
fmt.Printf("Loaded threads: %d\n", len(threads))
threadPostsMap, postCount := getThreadPostsMap()
fmt.Printf("Loaded posts: %d\n", postCount)
for i := 0; i < (len(threads)-1)/AddThreadsBatchSize+1; i++ {
start := i * AddThreadsBatchSize
end := (i + 1) * AddThreadsBatchSize
if end > len(threads) {
end = len(threads)
}
tmp := threads[start:end]
fmt.Printf("Add threads: [%d - %d].\n", start, end)
addThreads(tmp, threadPostsMap, attachmentMap, forumMap, classMap)
}
}

241
discuzx/topic.go Normal file
View File

@ -0,0 +1,241 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"strconv"
"github.com/casbin/casnode/object"
)
var MaxTopTime = "9999-00-00T00:00:00+08:00"
func getTopicFromThread(thread *Thread, forum *Forum, classMap map[int]*Class) *object.Topic {
content := ""
ip := ""
if thread.Posts[0].First == 1 {
content = thread.Posts[0].Message
ip = thread.Posts[0].Useip
} else {
panic("getTopicFromThread() error: thread.Posts[0].First != 1")
}
content = escapeContent(content)
content = addAttachmentsToContent(content, thread.Posts[0].UploadFileRecords)
tags := []string{}
if class, ok := classMap[thread.Typeid]; ok {
tags = append(tags, class.Name)
}
homePageTopTime := ""
tabTopTime := ""
nodeTopTime := ""
deleted := false
isHidden := false
state := ""
// https://blog.csdn.net/daily886/article/details/79569894
if thread.Displayorder == 3 {
homePageTopTime = MaxTopTime
} else if thread.Displayorder == 2 {
tabTopTime = MaxTopTime
} else if thread.Displayorder == 1 {
nodeTopTime = MaxTopTime
} else if thread.Displayorder == -1 {
deleted = true
} else if thread.Displayorder == -2 {
isHidden = true
state = "Reviewing"
} else if thread.Displayorder == -3 {
isHidden = true
state = "ReviewIgnored"
} else if thread.Displayorder == -4 {
isHidden = true
state = "Draft"
}
nodeId := strconv.Itoa(thread.Fid)
tabId := ""
if forum != nil {
nodeId = forum.Name
if forum.Type == "group" || forum.Parent == nil {
return nil
}
parentForum := forum.Parent
if parentForum.Parent != nil {
parentForum = parentForum.Parent
}
tabId = parentForum.Name
} else {
isHidden = true
}
topic := &object.Topic{
Id: thread.Tid,
Author: thread.Author,
NodeId: nodeId,
NodeName: nodeId,
TabId: tabId,
Title: thread.Subject,
CreatedTime: getTimeFromUnixSeconds(thread.Dateline),
Tags: tags,
LastReplyUser: thread.Lastposter,
LastReplyTime: getTimeFromUnixSeconds(thread.Lastpost),
ReplyCount: thread.Replies,
UpCount: thread.RecommendAdd,
DownCount: thread.RecommendSub,
HitCount: thread.Views,
Hot: thread.Heats,
FavoriteCount: thread.Favtimes,
HomePageTopTime: homePageTopTime,
TabTopTime: tabTopTime,
NodeTopTime: nodeTopTime,
Deleted: deleted,
Content: content,
IsHidden: isHidden,
Ip: ip,
State: state,
}
return topic
}
func getReplyFromPost(topicId int, post *Post) *object.Reply {
content := escapeContent(post.Message)
content = addAttachmentsToContent(content, post.UploadFileRecords)
deleted := false
isHidden := false
state := ""
// https://blog.csdn.net/fengda2870/article/details/8699229
if post.Invisible == -2 {
isHidden = true
state = "Reviewing"
} else if post.Invisible == -3 {
isHidden = true
state = "ReviewIgnored"
} else if post.Invisible == -5 {
deleted = true
}
reply := &object.Reply{
Id: post.Pid,
Author: post.Author,
TopicId: topicId,
CreatedTime: getTimeFromUnixSeconds(post.Dateline),
Deleted: deleted,
IsHidden: isHidden,
ThanksNum: 0,
Content: content,
Ip: post.Useip,
State: state,
}
return reply
}
func deleteWholeTopic(thread *Thread) {
topics := object.GetTopicsByTitleAndAuthor(thread.Subject, thread.Author)
for _, topic := range topics {
topicId := topic.Id
object.DeleteTopicHard(topicId)
object.DeleteFilesByMember(thread.Author)
//replies := object.GetReplies(topicId, "")
//for _, reply := range replies {
// object.DeleteFilesByMember(reply.Author)
//}
object.DeleteRepliesHardByTopicId(topicId)
}
}
func recordToHyperlink(record *object.UploadFileRecord) string {
if record.FileType == "image" {
return fmt.Sprintf("![%s](%s)\n", record.FileName, record.FileUrl)
} else {
return fmt.Sprintf("- [%s](%s)\n", record.FileName, record.FileUrl)
}
}
func addAttachmentsToContent(content string, records []*object.UploadFileRecord) string {
images := []*object.UploadFileRecord{}
files := []*object.UploadFileRecord{}
for _, record := range records {
if record.FileType == "image" {
images = append(images, record)
} else {
files = append(files, record)
}
}
if len(images) != 0 {
content += "\n\n### 图片:\n\n"
for _, record := range images {
content += recordToHyperlink(record)
}
return content
}
if len(files) != 0 {
content += "\n\n### 附件:\n\n"
for _, record := range files {
content += recordToHyperlink(record)
}
return content
}
return content
}
func getTopicAndReplies(thread *Thread, forum *Forum, classMap map[int]*Class) (*object.Topic, []*object.Reply) {
// remove leading useless posts
posts := []*Post{}
isBeforeFirstPosition := true
for _, post := range thread.Posts {
if !isBeforeFirstPosition || post.First == 1 {
isBeforeFirstPosition = false
posts = append(posts, post)
}
}
thread.Posts = posts
if len(thread.Posts) == 0 {
// thread is deleted.
return nil, nil
}
topic := getTopicFromThread(thread, forum, classMap)
if topic == nil {
// thread doesn't belong to any forum, ignore it
return nil, nil
}
replies := []*object.Reply{}
for i, post := range thread.Posts {
if i == 0 {
continue
}
//if post.First == 1 {
// panic(fmt.Errorf("getTopicAndReplies() error: thread.Posts[%d].First == 1", i))
//}
reply := getReplyFromPost(thread.Tid, post)
replies = append(replies, reply)
}
return topic, replies
}

132
discuzx/user.go Normal file
View File

@ -0,0 +1,132 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"strconv"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
func getUserFromMember(memberEx *MemberEx) *casdoorsdk.User {
user := &casdoorsdk.User{
Owner: CasdoorOrganization,
Name: memberEx.Member.Username,
CreatedTime: getTimeFromUnixSeconds(memberEx.Member.Regdate),
Id: strconv.Itoa(memberEx.Member.Uid),
Type: "normal-user",
// Password: memberEx.UcenterMember.Password,
// PasswordSalt: memberEx.UcenterMember.Salt,
// DisplayName: displayName,
Avatar: "",
PermanentAvatar: "*",
Email: memberEx.Member.Email,
// Phone: memberEx.Profile.Mobile,
// Location: memberEx.Profile.Residecity,
Address: []string{},
// Affiliation: memberEx.Profile.Occupation,
// Title: memberEx.Profile.Position,
// IdCardType: idCardType,
// IdCard: idCard,
// Homepage: memberEx.Profile.Site,
// Bio: memberEx.Profile.Bio,
// Tag: memberEx.Profile.Interest,
Region: "CN",
Language: "zh",
// Gender: gender,
// Birthday: birthday,
// Education: memberEx.Profile.Education,
Score: memberEx.Member.Credits,
Ranking: memberEx.Member.Uid,
IsOnline: false,
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,
IsDeleted: false,
SignupApplication: CasdoorApplication,
// CreatedIp: memberEx.UcenterMember.Regip,
// LastSigninTime: getTimeFromUnixSeconds(memberEx.UcenterMember.Lastlogintime),
LastSigninIp: "",
Properties: map[string]string{},
}
if memberEx.UcenterMember == nil {
fmt.Printf("[%d, %s] memberEx.UcenterMember == nil\n", memberEx.Member.Uid, memberEx.Member.Username)
} else {
user.Password = memberEx.UcenterMember.Password
user.PasswordSalt = memberEx.UcenterMember.Salt
user.CreatedIp = memberEx.UcenterMember.Regip
user.LastSigninTime = getTimeFromUnixSeconds(memberEx.UcenterMember.Lastlogintime)
}
if memberEx.Profile == nil {
fmt.Printf("[%d, %s] memberEx.Profile == nil\n", memberEx.Member.Uid, memberEx.Member.Username)
} else {
displayName := memberEx.Profile.Realname
if displayName == "" {
displayName = memberEx.Member.Username
}
idCardType := ""
idCard := ""
if memberEx.Profile.Idcard != "" {
idCardType = "IdCard"
idCard = memberEx.Profile.Idcard
}
gender := "Male"
if memberEx.Profile.Gender == 2 {
gender = "Female"
}
birthday := ""
if memberEx.Profile.Birthyear != 0 && memberEx.Profile.Birthmonth != 0 && memberEx.Profile.Birthday != 0 {
birthday = fmt.Sprintf("%02d-%02d-%02d", memberEx.Profile.Birthyear, memberEx.Profile.Birthmonth, memberEx.Profile.Birthday)
}
address := []string{}
if memberEx.Profile.Resideprovince != "" {
address = append(address, memberEx.Profile.Resideprovince)
if memberEx.Profile.Residecity != "" {
address = append(address, memberEx.Profile.Residecity)
if memberEx.Profile.Residedist != "" {
address = append(address, memberEx.Profile.Residedist)
if memberEx.Profile.Residecommunity != "" {
address = append(address, memberEx.Profile.Residecommunity)
}
}
}
}
user.Address = address
user.DisplayName = displayName
user.IdCardType = idCardType
user.IdCard = idCard
user.Gender = gender
user.Birthday = birthday
user.Phone = memberEx.Profile.Mobile
user.Location = memberEx.Profile.Residecity
user.Affiliation = memberEx.Profile.Occupation
user.Title = memberEx.Profile.Position
user.Homepage = memberEx.Profile.Site
user.Bio = memberEx.Profile.Bio
user.Tag = memberEx.Profile.Interest
user.Education = memberEx.Profile.Education
}
return user
}

59
discuzx/user_test.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"sync"
"testing"
"github.com/casbin/casnode/casdoor"
"github.com/casbin/casnode/controllers"
"github.com/casbin/casnode/object"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
var AddUsersConcurrency = 20
func TestAddUsers(t *testing.T) {
object.InitConfig()
InitAdapter()
object.InitAdapter()
casdoor.InitCasdoorAdapter()
controllers.InitAuthConfig()
membersEx := getMembersEx()
var wg sync.WaitGroup
wg.Add(len(membersEx))
sem := make(chan int, AddUsersConcurrency)
users := []*casdoorsdk.User{}
for i, memberEx := range membersEx {
sem <- 1
go func(i int, memberEx *MemberEx) {
defer wg.Done()
user := getUserFromMember(memberEx)
users = append(users, user)
fmt.Printf("[%d/%d]: Added user: [%d, %s]\n", i+1, len(membersEx), memberEx.Member.Uid, memberEx.Member.Username)
<-sem
}(i, memberEx)
}
wg.Wait()
casdoor.AddUsersInBatch(users)
}

180
discuzx/util.go Normal file
View File

@ -0,0 +1,180 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package discuzx
import (
"fmt"
"hash/fnv"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
"time"
)
var (
reBold *regexp.Regexp
reAlign *regexp.Regexp
reFont *regexp.Regexp
reUrl *regexp.Regexp
reSize *regexp.Regexp
reSize2 *regexp.Regexp
reSize3 *regexp.Regexp
reVideo *regexp.Regexp
)
func init() {
reBold, _ = regexp.Compile("\\[b](.*?)\\[/b]")
reAlign, _ = regexp.Compile("\\[align=([a-z]+)](.*?)\\[/align]")
reFont, _ = regexp.Compile("\\[font=([^]]+)](.*?)\\[/font]")
reUrl, _ = regexp.Compile("\\[url=([^]]+)](.*?)\\[/url]")
reSize, _ = regexp.Compile("\\[[a-z]+(=[^]]+)?]")
reSize2, _ = regexp.Compile("\\[/align]")
reSize3, _ = regexp.Compile("\\[/[a-z]+]")
// reSize, _ = regexp.Compile("\\[size=\\d+\\].*\\[/size\\]")
reVideo, _ = regexp.Compile("\\[media=x,(\\d+),(\\d+)\\].*/id_(.*)\\.html\\[/media\\]")
}
func getTimeFromUnixSeconds(t int) string {
tm := time.Unix(int64(t), 0)
return tm.Format(time.RFC3339)
}
func getYearFromUnixSeconds(t int) int {
tm := time.Unix(int64(t), 0)
return tm.Year()
}
func escapeVideo(text string) string {
// [media=x,500,375]https://v.youku.com/v_show/id_XNDU0NjEyODg0MA==.html[/media]
// <iframe height=498 width=510 src='https://player.youku.com/embed/XNDU0NjEyODg0MA==' frameborder=0 'allowfullscreen'></iframe>
text = reVideo.ReplaceAllString(text, "\n<iframe width=$1 height=$2 src='https://player.youku.com/embed/$3' frameborder=0 'allowfullscreen'></iframe>\n")
return text
}
func escapeContent(text string) string {
text = strings.ReplaceAll(text, "[quote]", "```\n")
text = strings.ReplaceAll(text, "[/quote]", "\n```")
text = reBold.ReplaceAllString(text, "<b>$1</b>")
text = reAlign.ReplaceAllString(text, "<p align=\"$1\">$2</p>")
text = reFont.ReplaceAllString(text, "<font face=\"$1\">$2</font>")
text = reUrl.ReplaceAllString(text, "[$2]($1)")
text = reSize.ReplaceAllString(text, "")
text = reSize2.ReplaceAllString(text, "\n")
text = reSize3.ReplaceAllString(text, "")
text = escapeVideo(text)
text = strings.ReplaceAll(text, "\n", "\n\n")
text = strings.ReplaceAll(text, "\r", "")
text = strings.ReplaceAll(text, "\n\n\n", "\n\n<br />\n\n")
return text
}
func getRedirectUrl(url string) string {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
panic(err)
}
newUrl := ""
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
newUrl = req.URL.String()
return http.ErrUseLastResponse
}
times := 0
for {
_, err = client.Do(req)
if err != nil {
times += 1
time.Sleep(3 * time.Second)
if times >= 10 {
panic(err)
}
} else {
break
}
}
if newUrl == "" {
newUrl = url
}
return newUrl
}
func downloadFile(url string) ([]byte, string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, "", err
}
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
newUrl := resp.Request.URL.String()
return bs, newUrl, nil
}
func downloadFileSafe(url string) ([]byte, string, error) {
var bs []byte
var newUrl string
var err error
times := 0
for {
bs, newUrl, err = downloadFile(url)
if err != nil {
times += 1
time.Sleep(3 * time.Second)
if times >= 10 {
return nil, "", err
}
} else {
break
}
}
return bs, newUrl, nil
}
func getStringHash(s string) int {
h := fnv.New32a()
h.Write([]byte(s))
return int(h.Sum32())
}
func getRandomAvatarUrl(s string) string {
i := getStringHash(s)
i = i%244 + 1
avatarUrl := fmt.Sprintf("%s%d.png", avatarPoolBaseUrl, i)
return avatarUrl
}

24
docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
version: '3.1'
services:
casnode:
build:
context: ./
dockerfile: Dockerfile
ports:
- "7000:7000"
depends_on:
- db
volumes:
- ./conf:/conf/
db:
restart: always
image: mysql:8.0.25
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- /usr/local/docker/mysqls:/var/lib/mysql

36
go.mod Normal file
View File

@ -0,0 +1,36 @@
module github.com/casbin/casnode
go 1.16
require (
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d // indirect
github.com/astaxie/beego v1.12.3
github.com/casbin/google-groups-crawler v0.1.3
github.com/casdoor/casdoor-go-sdk v0.10.0
github.com/chromedp/chromedp v0.8.4
github.com/elazarl/go-bindata-assetfs v1.0.1 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/golang/protobuf v1.5.2 // indirect
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd
github.com/huichen/sego v0.0.0-20180617034105-3f3c8a8cfacc
github.com/issue9/assert v1.4.1
github.com/lib/pq v1.10.2 // indirect
github.com/microcosm-cc/bluemonday v1.0.5
github.com/mileusna/crontab v1.0.1
github.com/mozillazg/go-slugify v0.2.0
github.com/mozillazg/go-unidecode v0.1.1 // indirect
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
github.com/prometheus/client_golang v1.11.0 // indirect
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/sromku/go-gitter v0.0.0-20170828210750-70f7030a94a6
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
golang.org/x/text v0.3.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
xorm.io/core v0.7.2
xorm.io/xorm v0.8.1
)

650
go.sum Normal file
View File

@ -0,0 +1,650 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d h1:ir/IFJU5xbja5UaBEQLjcvn7aAU01nqU/NUyOBEU+ew=
github.com/adamzy/cedar-go v0.0.0-20170805034717-80a9c64b256d/go.mod h1:PRWNwWq0yifz6XDPZu48aSld8BWwBfr2JKB2bGWiEd4=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
github.com/casbin/google-groups-crawler v0.1.3 h1:kmbzjLK88dtSTk7ycDvjKH6hwVB0z6dAJGpJvvqRFsg=
github.com/casbin/google-groups-crawler v0.1.3/go.mod h1:JHKvWP8blOe/Mbob3R4aaU5RvVIOC83eBcCSlKsbKSI=
github.com/casdoor/casdoor-go-sdk v0.10.0 h1:gakpRFtGgesKhNb002C2bf66cUb+1uZzj8eEaRKpcJU=
github.com/casdoor/casdoor-go-sdk v0.10.0/go.mod h1:MBed3ISHQfXTtoOCAk5T8l5lt4wFvsyynrw0awggydY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
github.com/chromedp/cdproto v0.0.0-20220812200530-d0d83820bffc h1:xGhCpiX5oNMoZGnzYvv1ne4muVRl2SDHH5fL7oUbZAY=
github.com/chromedp/cdproto v0.0.0-20220812200530-d0d83820bffc/go.mod h1:5Y4sD/eXpwrChIuxhSr/G20n9CdbCmoerOHnuAf0Zr0=
github.com/chromedp/chromedp v0.8.4 h1:50NSLCXC38kGsCmLCi8tXZ5V5FtQ3BOV/90u1O06Gq4=
github.com/chromedp/chromedp v0.8.4/go.mod h1:ikuiJrEMoOnTPFUBu6jV0XZFOGW6W/Nhgk/OZyvh00o=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4 h1:YcpmyvADGYw5LqMnHqSkyIELsHCGF6PkrmM31V8rF7o=
github.com/denisenkom/go-mssqldb v0.0.0-20190707035753-2be1aa521ff4/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:9wScpmSP5A3Bk8V3XHWUcJmYTh+ZnlHVyc+A4oZYS3Y=
github.com/go-xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:56xuuqnHyryaerycW3BfssRdxQstACi0Epw/yC5E2xM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd h1:0b8AqsWQb6A0jjx80UXLG/uMTXQkGD0IGuXWqsrNz1M=
github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huichen/sego v0.0.0-20180617034105-3f3c8a8cfacc h1:3LXYtoxQGFSjIL5ZJAn4PceSpwRohuTKYL1W4kJ7G8g=
github.com/huichen/sego v0.0.0-20180617034105-3f3c8a8cfacc/go.mod h1:+/Bm7uk1bnJJMi9l6P88FgHeGtscOQiYbxW1j+BmgBY=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/issue9/assert v1.4.1 h1:gUtOpMTeaE4JTe9kACma5foOHBvVt1p5XTFrULDwdXI=
github.com/issue9/assert v1.4.1/go.mod h1:Yktk83hAVl1SPSYtd9kjhBizuiBIqUQyj+D5SE2yjVY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w=
github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/mileusna/crontab v1.0.1 h1:YrDLc7l3xOiznmXq2FtAgg+1YQ3yC6pfFVPe+ywXNtg=
github.com/mileusna/crontab v1.0.1/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4U1ZwExmc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozillazg/go-slugify v0.2.0 h1:SIhqDlnJWZH8OdiTmQgeXR28AOnypmAXPeOTcG7b9lk=
github.com/mozillazg/go-slugify v0.2.0/go.mod h1:z7dPH74PZf2ZPFkyxx+zjPD8CNzRJNa1CGacv0gg8Ns=
github.com/mozillazg/go-unidecode v0.1.1 h1:uiRy1s4TUqLbcROUrnCN/V85Jlli2AmDF6EeAXOeMHE=
github.com/mozillazg/go-unidecode v0.1.1/go.mod h1:fYMdhyjni9ZeEmS6OE/GJHDLsF8TQvIVDwYR/drR26Q=
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d h1:tLWCMSjfL8XyZwpu1RzI2UpJSPbZCOZ6DVHQFnlpL7A=
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g=
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/sromku/go-gitter v0.0.0-20170828210750-70f7030a94a6 h1:7AV47xvYbuwoNxR9LDhkRwqzZsySCX5H8WVM4zrDmME=
github.com/sromku/go-gitter v0.0.0-20170828210750-70f7030a94a6/go.mod h1:P2BoF5QlNE1UcKtYKP8xa8B9I5eALYU5JpRdCqLddL4=
github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
xorm.io/builder v0.3.6 h1:ha28mQ2M+TFx96Hxo+iq6tQgnkC9IZkM6D8w9sKHHF8=
xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU=
xorm.io/core v0.7.2 h1:mEO22A2Z7a3fPaZMk6gKL/jMD80iiyNwRrX5HOv3XLw=
xorm.io/core v0.7.2/go.mod h1:jJfd0UAEzZ4t87nbQYtVjmqpIODugN6PD2D9E+dJvdM=
xorm.io/xorm v0.8.1 h1:4f2KXuQxVdaX3RdI3Fw81NzMiSpZeyCZt8m3sEVeIkQ=
xorm.io/xorm v0.8.1/go.mod h1:ZkJLEYLoVyg7amJK/5r779bHyzs2AU8f8VMiP6BM7uY=

97
i18n/generate.go Normal file
View File

@ -0,0 +1,97 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/casbin/casnode/util"
)
type I18nData map[string]map[string]string
var reI18n *regexp.Regexp
func init() {
reI18n, _ = regexp.Compile("i18next.t\\(\"(.*?)\"\\)")
}
func getAllI18nStrings(fileContent string) []string {
res := []string{}
matches := reI18n.FindAllStringSubmatch(fileContent, -1)
if matches == nil {
return res
}
for _, match := range matches {
res = append(res, match[1])
}
return res
}
func getAllJsFilePaths() []string {
path := "../web/src"
res := []string{}
err := filepath.Walk(path,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !strings.HasSuffix(info.Name(), ".js") {
return nil
}
res = append(res, path)
fmt.Println(path, info.Name())
return nil
})
if err != nil {
panic(err)
}
return res
}
func parseToData() *I18nData {
allWords := []string{}
paths := getAllJsFilePaths()
for _, path := range paths {
fileContent := util.ReadStringFromPath(path)
words := getAllI18nStrings(fileContent)
allWords = append(allWords, words...)
}
fmt.Printf("%v\n", allWords)
data := I18nData{}
for _, word := range allWords {
tokens := strings.Split(word, ":")
namespace := tokens[0]
key := tokens[1]
if _, ok := data[namespace]; !ok {
data[namespace] = map[string]string{}
}
data[namespace][key] = key
}
return &data
}

39
i18n/generate_test.go Normal file
View File

@ -0,0 +1,39 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import "testing"
func applyToOtherLanguage(dataEn *I18nData, lang string) {
dataOther := readI18nFile(lang)
println(dataOther)
applyData(dataEn, dataOther)
writeI18nFile(lang, dataEn)
}
func TestGenerateI18nStrings(t *testing.T) {
dataEn := parseToData()
writeI18nFile("en", dataEn)
applyToOtherLanguage(dataEn, "de")
applyToOtherLanguage(dataEn, "fr")
applyToOtherLanguage(dataEn, "ja")
applyToOtherLanguage(dataEn, "ko")
applyToOtherLanguage(dataEn, "ru")
applyToOtherLanguage(dataEn, "zh")
applyToOtherLanguage(dataEn, "zh-TW")
applyToOtherLanguage(dataEn, "kk")
}

62
i18n/util.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package i18n
import (
"fmt"
"github.com/casbin/casnode/util"
)
func getI18nFilePath(language string) string {
return fmt.Sprintf("../web/src/locales/%s/data.json", language)
}
func readI18nFile(language string) *I18nData {
s := util.ReadStringFromPath(getI18nFilePath(language))
data := &I18nData{}
err := util.JsonToStruct(s, data)
if err != nil {
panic(err)
}
return data
}
func writeI18nFile(language string, data *I18nData) {
s := util.StructToJsonFormatted(data)
s = s + "\n"
println(s)
util.WriteStringToPath(s, getI18nFilePath(language))
}
func applyData(data1 *I18nData, data2 *I18nData) {
for namespace, pairs2 := range *data2 {
if _, ok := (*data1)[namespace]; !ok {
continue
}
pairs1 := (*data1)[namespace]
for key, value := range pairs2 {
if _, ok := pairs1[key]; !ok {
continue
}
pairs1[key] = value
}
}
}

66
main.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"github.com/astaxie/beego"
"github.com/astaxie/beego/plugins/cors"
_ "github.com/astaxie/beego/session/redis"
"github.com/casbin/casnode/casdoor"
"github.com/casbin/casnode/object"
"github.com/casbin/casnode/routers"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
)
func main() {
object.InitAdapter()
object.InitHttpClient()
casdoor.InitCasdoorAdapter()
service.InitDictionary()
util.InitSegmenter()
object.InitForumBasicInfo()
object.InitFrontConf()
object.InitTimer()
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "PUT", "PATCH"},
AllowHeaders: []string{"Origin"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
// beego.DelStaticPath("/static")
beego.SetStaticPath("/static", "web/build/static")
beego.SetStaticPath("/swagger", "swagger")
beego.BConfig.WebConfig.DirectoryIndex = true
// https://studygolang.com/articles/2303
beego.InsertFilter("*", beego.BeforeRouter, routers.BotFilter)
beego.InsertFilter("*", beego.BeforeRouter, routers.Static)
beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
if beego.AppConfig.String("redisEndpoint") == "" {
beego.BConfig.WebConfig.Session.SessionProvider = "file"
beego.BConfig.WebConfig.Session.SessionProviderConfig = "./tmp"
} else {
beego.BConfig.WebConfig.Session.SessionProvider = "redis"
beego.BConfig.WebConfig.Session.SessionProviderConfig = beego.AppConfig.String("redisEndpoint")
}
beego.BConfig.WebConfig.Session.SessionGCMaxLifetime = 3600 * 24 * 30
beego.Run()
}

196
object/adapter.go Normal file
View File

@ -0,0 +1,196 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"runtime"
"github.com/astaxie/beego"
_ "github.com/go-sql-driver/mysql"
"xorm.io/xorm"
)
var (
adapter *Adapter
CasdoorOrganization string
CasdoorApplication string
)
type Session struct {
SessionKey string `xorm:"char(64) notnull pk"`
SessionData []uint8 `xorm:"blob"`
SessionExpiry int `xorm:"notnull"`
}
func InitConfig() {
err := beego.LoadAppConfig("ini", "../conf/app.conf")
if err != nil {
panic(err)
}
}
func InitAdapter() {
adapter = NewAdapter(beego.AppConfig.String("driverName"), beego.AppConfig.String("dataSourceName"), beego.AppConfig.String("dbName"))
adapter.createTable()
CasdoorOrganization = beego.AppConfig.String("casdoorOrganization")
CasdoorApplication = beego.AppConfig.String("casdoorApplication")
}
// Adapter represents the MySQL adapter for policy storage.
type Adapter struct {
driverName string
dataSourceName string
dbName string
Engine *xorm.Engine
}
// finalizer is the destructor for Adapter.
func finalizer(a *Adapter) {
err := a.Engine.Close()
if err != nil {
panic(err)
}
}
// NewAdapter is the constructor for Adapter.
func NewAdapter(driverName string, dataSourceName string, dbName string) *Adapter {
a := &Adapter{}
a.driverName = driverName
a.dataSourceName = dataSourceName
a.dbName = dbName
// Open the DB, create it if not existed.
a.open()
// Call the destructor when the object is released.
runtime.SetFinalizer(a, finalizer)
return a
}
func (a *Adapter) createDatabase() error {
Engine, err := xorm.NewEngine(a.driverName, a.dataSourceName)
if err != nil {
return err
}
defer Engine.Close()
_, err = Engine.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s default charset utf8 COLLATE utf8_general_ci", a.dbName))
return err
}
func (a *Adapter) open() {
if a.driverName != "postgres" {
if err := a.createDatabase(); err != nil {
panic(err)
}
}
Engine, err := xorm.NewEngine(a.driverName, a.dataSourceName+a.dbName)
if err != nil {
panic(err)
}
a.Engine = Engine
}
func (a *Adapter) close() {
a.Engine.Close()
a.Engine = nil
}
func (a *Adapter) createTable() {
err := a.Engine.Sync2(new(Session))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Topic))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Reply))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Poster))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Translator))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Node))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Favorites))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Tab))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Notification))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(BasicInfo))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Plane))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(ConsumptionRecord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(BrowseRecord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(UploadFileRecord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(SensitiveWord))
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(FrontConf))
if err != nil {
panic(err)
}
}

27
object/avatar.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"github.com/astaxie/beego"
)
var CasdoorStorageEndpoint = beego.AppConfig.String("casdoorStorageEndpoint")
func getUserAvatar(username string) string {
return fmt.Sprintf("%scasdoor/avatar/%s/%s.png", CasdoorStorageEndpoint, CasdoorOrganization, username)
}

263
object/balance.go Normal file
View File

@ -0,0 +1,263 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"sync"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
// ConsumptionType 1-9 means:
// login bonus, receive thanks(topic), receive thanks(reply), thanks(topic)
// thanks(reply), new reply, receive reply bonus, new topic, top topic.
type ConsumptionRecord struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
Amount int `xorm:"int" json:"amount"`
Balance int `xorm:"int" json:"balance"`
ConsumerId string `xorm:"varchar(100) index" json:"consumerId"`
ObjectId int `xorm:"int index" json:"objectId"`
ReceiverId string `xorm:"varchar(100) index" json:"receiverId"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
ConsumptionType int `xorm:"int" json:"consumptionType"`
}
func GetBalances() []*ConsumptionRecord {
balances := []*ConsumptionRecord{}
err := adapter.Engine.Desc("created_time").Find(&balances)
if err != nil {
panic(err)
}
return balances
}
func GetMemberBalances(id string, limit, offset int) []*ConsumptionRecord {
balances := []*ConsumptionRecord{}
err := adapter.Engine.Desc("created_time").Where("receiver_id = ?", id).Find(&balances)
if err != nil {
panic(err)
}
return balances
}
func AddBalance(balance *ConsumptionRecord) bool {
affected, err := adapter.Engine.Insert(balance)
if err != nil {
panic(err)
}
return affected != 0
}
func GetMemberBalance(user *casdoorsdk.User) int {
return user.Score
}
func UpdateMemberBalance(user *casdoorsdk.User, amount int) (bool, error) {
user.Score += amount
return casdoorsdk.UpdateUserForColumns(user, []string{"score"})
}
func UpdateMemberConsumptionSum(user *casdoorsdk.User, amount int) (bool, error) {
user.Karma += amount
return casdoorsdk.UpdateUserForColumns(user, []string{"karma"})
}
func GetMemberConsumptionRecordNum(memberId string) int {
var total int64
var err error
record := new(ConsumptionRecord)
total, err = adapter.Engine.Where("receiver_id = ?", memberId).Count(record)
if err != nil {
panic(err)
}
return int(total)
}
func GetMemberConsumptionRecord(id string, limit, offset int) []*BalanceResponse {
record := GetMemberBalances(id, limit, offset)
var wg sync.WaitGroup
errChan := make(chan error, limit+1)
res := make([]*BalanceResponse, len(record))
for k, v := range record {
wg.Add(1)
go func(k int, v *ConsumptionRecord) {
defer wg.Done()
tempRecord := BalanceResponse{
Amount: v.Amount,
ConsumerId: v.ConsumerId,
ReceiverId: v.ReceiverId,
Balance: v.Balance,
CreatedTime: v.CreatedTime,
ConsumptionType: v.ConsumptionType,
}
switch v.ConsumptionType {
case 2:
tempRecord.Title = GetTopicTitle(v.ObjectId)
case 4:
tempRecord.Title = GetTopicTitle(v.ObjectId)
if len(tempRecord.Title) == 0 {
tempRecord.ConsumptionType = 10
break
}
case 6:
fallthrough
case 3:
fallthrough
case 5:
fallthrough
case 7:
replyInfo := GetReply(v.ObjectId)
if replyInfo == nil || replyInfo.Deleted {
tempRecord.ConsumptionType = 10
break
}
topicInfo := GetTopic(replyInfo.TopicId)
if topicInfo == nil || topicInfo.Deleted {
tempRecord.ConsumptionType = 10
break
}
tempRecord.Title = topicInfo.Title
tempRecord.ObjectId = topicInfo.Id
tempRecord.Length = len(replyInfo.Content)
case 8:
fallthrough
case 9:
topicInfo := GetTopic(v.ObjectId)
if topicInfo == nil || topicInfo.Deleted {
tempRecord.ConsumptionType = 10
break
}
tempRecord.ObjectId = v.ObjectId
tempRecord.Title = topicInfo.Title
tempRecord.Length = len(topicInfo.Content)
}
res[k] = &tempRecord
}(k, v)
}
wg.Wait()
close(errChan)
if len(errChan) != 0 {
for v := range errChan {
panic(v)
}
}
return res
}
func GetThanksStatus(memberId string, id, recordType int) bool {
record := new(ConsumptionRecord)
total, err := adapter.Engine.Where("consumption_type = ?", recordType).And("object_id = ?", id).And("receiver_id = ?", memberId).Count(record)
if err != nil {
panic(err)
}
return total != 0
}
func CreateTopicConsumption(user *casdoorsdk.User, id int) bool {
record := ConsumptionRecord{
// Id: util.IntToString(GetConsumptionRecordId()),
ReceiverId: GetUserName(user),
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
ConsumptionType: 8,
}
record.Amount = CreateTopicCost
record.Amount = -record.Amount
balance := GetMemberBalance(user)
if balance+record.Amount < 0 {
return false
}
record.Balance = balance + record.Amount
AddBalance(&record)
UpdateMemberBalance(user, record.Amount)
UpdateMemberConsumptionSum(user, -record.Amount)
return true
}
func CreateReplyConsumption(user *casdoorsdk.User, id int) bool {
record := ConsumptionRecord{
// Id: util.IntToString(GetConsumptionRecordId()),
ReceiverId: GetUserName(user),
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
ConsumptionType: 6,
}
record.Amount = CreateReplyCost
record.Amount = -record.Amount
balance := GetMemberBalance(user)
if balance+record.Amount < 0 {
return false
}
record.Balance = balance + record.Amount
AddBalance(&record)
UpdateMemberBalance(user, record.Amount)
UpdateMemberConsumptionSum(user, -record.Amount)
return true
}
func GetReplyBonus(author *casdoorsdk.User, consumer *casdoorsdk.User, id int) {
if author.Name == consumer.Name {
return
}
record := ConsumptionRecord{
// Id: util.IntToString(GetConsumptionRecordId()),
ConsumerId: consumer.Name,
ReceiverId: author.Name,
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
ConsumptionType: 7,
}
record.Amount = ReceiveReplyBonus
balance := GetMemberBalance(consumer)
record.Balance = balance + record.Amount
AddBalance(&record)
UpdateMemberBalance(author, record.Amount)
}
func TopTopicConsumption(user *casdoorsdk.User, id int) bool {
record := ConsumptionRecord{
ReceiverId: GetUserName(user),
ObjectId: id,
CreatedTime: util.GetCurrentTime(),
ConsumptionType: 9,
}
record.Amount = TopTopicCost
record.Amount = -record.Amount
balance := GetMemberBalance(user)
if balance+record.Amount < 0 {
return false
}
record.Balance = balance + record.Amount
AddBalance(&record)
UpdateMemberBalance(user, record.Amount)
UpdateMemberConsumptionSum(user, -record.Amount)
return true
}

249
object/basic.go Normal file
View File

@ -0,0 +1,249 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"regexp"
"strings"
"github.com/casbin/casnode/util"
)
type BasicInfo struct {
Id string `xorm:"varchar(100) notnull pk"`
Value string `xorm:"mediumtext"`
}
var (
fileDate, version string
onlineMemberNum, highestOnlineNum int
)
func InitForumBasicInfo() {
GetForumVersion()
GetHighestOnlineNum()
UpdateOnlineMemberNum()
if AutoSyncPeriodSecond >= 30 {
fmt.Println("Auto sync from google group enabled.")
go AutoSyncGoogleGroup()
fmt.Println("Auto sync from gitter room enabled.")
go AutoSyncGitter()
} else {
fmt.Println("Auto sync from google group disabled.")
fmt.Println("Auto sync from gitter room disabled.")
}
}
func GetForumVersion() string {
pwd, _ := os.Getwd()
fileInfos, err := ioutil.ReadDir(pwd + "/.git/refs/heads")
for _, v := range fileInfos {
if v.Name() == "master" {
if v.ModTime().String() == fileDate {
return version
} else {
fileDate = v.ModTime().String()
break
}
}
}
content, err := ioutil.ReadFile(pwd + "/.git/refs/heads/master")
if err != nil {
return ""
}
// Convert to full length
temp := string(content)
version = strings.ReplaceAll(temp, "\n", "")
return version
}
func GetHighestOnlineNum() int {
if highestOnlineNum != 0 {
return highestOnlineNum
}
info := BasicInfo{Id: "HighestOnlineNum"}
existed, err := adapter.Engine.Get(&info)
if err != nil {
panic(err)
}
if existed {
highestOnlineNum = util.ParseInt(info.Value)
return highestOnlineNum
} else {
info := BasicInfo{
Id: "HighestOnlineNum",
Value: "0",
}
_, err := adapter.Engine.Insert(&info)
if err != nil {
panic(err)
}
return 0
}
}
func UpdateHighestOnlineNum(num int) bool {
highestOnlineNum = num
info := new(BasicInfo)
info.Value = util.IntToString(num)
affected, err := adapter.Engine.Where("id = ?", "HighestOnlineNum").Cols("value").Update(info)
if err != nil {
panic(err)
}
return affected != 0
}
func GetCronJobs() []*CronJob {
info := BasicInfo{Id: "CronJobs"}
existed, err := adapter.Engine.Get(&info)
if err != nil {
panic(err)
}
if existed {
var jobs []*CronJob
err := json.Unmarshal([]byte(info.Value), &jobs)
if err != nil {
panic(err)
}
return jobs
} else {
jobs, err := json.Marshal(DefaultCronJobs)
if err != nil {
panic(err)
}
info := BasicInfo{
Id: "CronJobs",
Value: string(jobs),
}
_, err = adapter.Engine.Insert(&info)
if err != nil {
panic(err)
}
return DefaultCronJobs
}
}
func GetCronUpdateJobs() []*UpdateJob {
info := BasicInfo{Id: "CronUpdateJobs"}
existed, err := adapter.Engine.Get(&info)
if err != nil {
panic(err)
}
if existed {
var posts []*UpdateJob
err := json.Unmarshal([]byte(info.Value), &posts)
if err != nil {
panic(err)
}
return posts
} else {
posts, err := json.Marshal(DefaultCronUpdates)
if err != nil {
panic(err)
}
info := BasicInfo{
Id: "CronUpdateJobs",
Value: string(posts),
}
_, err = adapter.Engine.Insert(&info)
if err != nil {
panic(err)
}
return DefaultCronUpdates
}
}
func GetLatestSyncedRecordId() int {
info := BasicInfo{Id: "LatestSyncedRecordId"}
existed, err := adapter.Engine.Get(&info)
if err != nil {
panic(err)
}
if existed {
return util.ParseInt(info.Value)
} else {
info := BasicInfo{
Id: "LatestSyncedRecordId",
Value: "0",
}
_, err := adapter.Engine.Insert(&info)
if err != nil {
panic(err)
}
return 0
}
}
func UpdateLatestSyncedRecordId(id int) bool {
info := new(BasicInfo)
info.Value = util.IntToString(id)
affected, err := adapter.Engine.Where("id = ?", "LatestSyncedRecordId").Cols("value").Update(info)
if err != nil {
panic(err)
}
return affected != 0
}
// GetOnlineMemberNum returns online member num.
func GetOnlineMemberNum() int {
if onlineMemberNum == 0 {
UpdateOnlineMemberNum()
return onlineMemberNum
}
if onlineMemberNum > highestOnlineNum {
UpdateHighestOnlineNum(onlineMemberNum)
}
return onlineMemberNum
}
// UpdateOnlineMemberNum updates online member num and updates highest online member num at the same time.
func UpdateOnlineMemberNum() {
onlineMemberNum = GetOnlineUserCount()
if onlineMemberNum > highestOnlineNum {
UpdateHighestOnlineNum(onlineMemberNum)
}
}
var recognizeHtmlTags *regexp.Regexp
func RemoveHtmlTags(text string) string {
if recognizeHtmlTags == nil {
recognizeHtmlTags = regexp.MustCompile("<[^>]+>")
}
return recognizeHtmlTags.ReplaceAllString(text, "")
}

59
object/check.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/microcosm-cc/bluemonday"
)
func HasNode(id string) bool {
node := GetNode(id)
return node != nil
}
func HasTab(id string) bool {
tab := GetTab(id)
return tab != nil
}
func HasPlane(id string) bool {
plane := GetPlane(id)
return plane != nil
}
// IsForbidden check member whether is forbidden.
func IsForbidden(user *casdoorsdk.User) bool {
return user.IsForbidden
}
func FilterUnsafeHTML(content string) string {
if content == "" {
return content
}
p := bluemonday.UGCPolicy()
p.AllowAttrs("style").OnElements("span")
p.AllowElements("video")
p.AllowAttrs("width").OnElements("video")
p.AllowAttrs("controls").OnElements("video")
p.AllowAttrs("src").OnElements("source")
p.AllowAttrs("type").OnElements("source")
p.AllowAttrs("style").OnElements("video")
res := p.Sanitize(content)
return res
}

90
object/conf.go Normal file
View File

@ -0,0 +1,90 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/astaxie/beego"
var (
DefaultPageNum = 20
DefaultHomePageNum = 50
DefaultNotificationPageNum = 10
DefaultBalancePageNum = 25
DefaultFilePageNum = 25
DefaultMemberAdminPageNum = 100
DefaultRenameQuota = 3
UserNamingRestrictions = false
HomePageNodeNum = 8
TopicThanksCost = 15
ReplyThanksCost = 10
CreateTopicCost = 20
CreateReplyCost = 5
TopTopicCost = 200
ReceiveReplyBonus = 5
MaxDailyCheckinBonus = 20
LatestNodeNum = 20
HotNodeNum = 15
HotTopicNum = 10
TopicEditableTime = 10.0 // minutes
ReplyEditableTime = 10.0 // minutes
ReplyDeletableTime = 5.0 // minutes
NodeHitRecordExpiredTime = 1 // month
TopicHitRecordExpiredTime = 1 // day
ValidateCodeExpiredTime = 20 // minutes
DefaultTopTopicTime = 10 // minutes
OnlineMemberExpiedTime = 10 // minutes
DefaultUploadFileQuota = 50
Domain = beego.AppConfig.String("domain") // domain
AutoSyncPeriodSecond = -1 // auto sync is disabled if < 30
DefaultCronJobs = []*CronJob{
{
Id: "updateExpiredData",
BumpTime: "0:0",
State: "active",
},
{
Id: "updateHotInfo",
BumpTime: "*/1:*",
State: "active",
},
{
Id: "expireData",
BumpTime: "*/1:*",
State: "active",
},
}
DefaultCronUpdates = []*UpdateJob{
{
Id: "expireData",
JobId: "updateExpiredData",
State: "active",
},
{
Id: "hotInfo",
JobId: "updateHotInfo",
State: "active",
},
{
Id: "expireTopTopic",
JobId: "expireData",
State: "active",
},
{
Id: "expireOnlineMember",
JobId: "expireData",
State: "active",
},
}
)

142
object/cron.go Executable file
View File

@ -0,0 +1,142 @@
package object
import (
"fmt"
"strings"
"time"
"github.com/mileusna/crontab"
"github.com/casbin/casnode/util"
)
type CronJob struct {
Id string `json:"id"`
BumpTime string `json:"bumpTime"`
State string `json:"state"`
}
type UpdateJob struct {
Id string `json:"id"`
JobId string `json:"jobId"`
State string `json:"state"`
Url string `json:"url"`
Content string `json:"content"`
}
var ctab *crontab.Crontab
func init() {
ctab = crontab.New()
}
func schedulePost(postId string) {
post := GetUpdateJob(postId)
isUpdated, num := post.updateInfo()
if isUpdated && num != 0 {
fmt.Printf("Update forum info: %s, update num: %d\n", post.Id, num)
}
}
func (job *UpdateJob) updateInfo() (bool, int) {
var num int
switch job.Id {
case "expireData":
expiredNodeDate := util.GetTimeMonth(-NodeHitRecordExpiredTime)
expiredTopicDate := util.GetTimeDay(-TopicHitRecordExpiredTime)
updateNodeNum := ChangeExpiredDataStatus(1, expiredNodeDate)
updateTopicNum := ChangeExpiredDataStatus(2, expiredTopicDate)
num = updateNodeNum + updateTopicNum
case "hotInfo":
last := GetLastRecordId()
latest := GetLatestSyncedRecordId()
if last == latest {
num = 0
} else {
UpdateLatestSyncedRecordId(last)
updateNodeNum := UpdateHotNode(latest)
updateTopicNum := UpdateHotTopic(latest)
num = updateTopicNum + updateNodeNum
}
case "expireTopTopic":
num = ExpireTopTopic()
}
return true, num
}
func GetUpdateJob(id string) *UpdateJob {
posts := GetCronUpdateJobs()
for _, v := range posts {
if v.Id == id {
return v
}
}
return &UpdateJob{}
}
func GetJobs() []*CronJob {
return GetCronJobs()
}
func GetUpdateJobs(jobId string) []*UpdateJob {
posts := GetCronUpdateJobs()
var jobs []*UpdateJob
for _, v := range posts {
if v.JobId == jobId {
jobs = append(jobs, v)
}
}
return jobs
}
func parseDumpTime(bumpTime string) (string, string) {
tokens := strings.Split(bumpTime, ":")
return tokens[0], tokens[1]
}
func refreshCronTasks() bool {
ctab.Clear()
jobs := GetJobs()
for _, job := range jobs {
if job.State != "active" || job.BumpTime == "" {
continue
}
hours, minutes := parseDumpTime(job.BumpTime)
posts := GetUpdateJobs(job.Id)
for _, post := range posts {
if post.State != "active" {
continue
}
schedule := fmt.Sprintf("%s %s * * *", minutes, hours)
// schedule := "* * * * *"
err := ctab.AddJob(schedule, schedulePost, post.Id)
if err != nil {
panic(err)
}
}
}
return true
}
func timerRoutine() {
for range time.Tick(time.Second * 3600) {
refreshCronTasks()
}
}
// InitTimer initializes scheduled tasks.
func InitTimer() {
refreshCronTasks()
go timerRoutine()
}

267
object/favorites.go Normal file
View File

@ -0,0 +1,267 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
type Favorites struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
FavoritesType string `xorm:"varchar(100) index" json:"favoritesType"`
ObjectId string `xorm:"varchar(100) index" json:"objectId"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
MemberId string `xorm:"varchar(100) index" json:"memberId"`
}
const (
FavorTopic = "favor_topic"
FollowUser = "follow_user"
FavorNode = "favor_node"
SubscribeTopic = "subscribe_topic"
)
func IsFavoritesExist(Type string) bool {
// check the if the string is in the enum
if Type == FavorTopic || Type == FollowUser || Type == FavorNode || Type == SubscribeTopic {
return true
}
return false
}
func AddFavorites(favorite *Favorites) bool {
affected, err := adapter.Engine.Insert(favorite)
if err != nil {
panic(err)
}
return affected != 0
}
func AddMemberFavorites(memberId string, favoritesType string, objectId string) bool {
status := GetFavoritesStatus(memberId, favoritesType, objectId)
if status == true {
return true
}
favorite := Favorites{
FavoritesType: favoritesType,
ObjectId: objectId,
CreatedTime: util.GetCurrentTime(),
MemberId: memberId,
}
return AddFavorites(&favorite)
}
func DeleteFavorites(memberId string, objectId string, favoritesType string) bool {
affected, err := adapter.Engine.Where("favorites_type = ?", favoritesType).And("object_id = ?", objectId).And("member_id = ?", memberId).Delete(&Favorites{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetFavoritesCount() int {
count, err := adapter.Engine.Count(&Favorites{})
if err != nil {
panic(err)
}
return int(count)
}
func GetFavoritesStatus(memberId string, objectId string, favoritesType string) bool {
node := new(Favorites)
total, err := adapter.Engine.Where("favorites_type = ?", favoritesType).And("object_id = ?", objectId).And("member_id = ?", memberId).Count(node)
if err != nil {
panic(err)
}
return total != 0
}
func GetTopicsFromFavorites(memberId string, limit int, offset int, favoritesType string) []*TopicWithAvatar {
favorites := []*Favorites{}
err := adapter.Engine.Where("member_id = ?", memberId).And("favorites_type = ?", favoritesType).Limit(limit, offset).Find(&favorites)
if err != nil {
panic(err)
}
topics := []*TopicWithAvatar{}
for _, v := range favorites {
topicId := util.ParseInt(v.ObjectId)
temp := GetTopicWithAvatar(topicId, nil)
topics = append(topics, temp)
}
return topics
}
func GetMembersFromFavorites(objectId string, favoritesType string) []*casdoorsdk.User {
favorites := []*Favorites{}
err := adapter.Engine.Where("object_id = ?", objectId).And("favorites_type = ?", favoritesType).Find(&favorites)
if err != nil {
panic(err)
}
members := []*casdoorsdk.User{}
for _, v := range favorites {
user := GetUser(v.MemberId)
if user != nil {
members = append(members, user)
}
}
return members
}
func GetRepliesFromFavorites(memberId string, limit int, offset int, favoritesType string) []*ReplyWithAvatar {
favorites := []*Favorites{}
err := adapter.Engine.Where("member_id = ?", memberId).And("favorites_type = ?", favoritesType).Limit(limit, offset).Find(&favorites)
if err != nil {
panic(err)
}
replies := []*ReplyWithAvatar{}
for _, v := range favorites {
topicId := util.ParseInt(v.ObjectId)
temp, _ := GetReplies(topicId, nil, limit, offset)
for _, v := range temp {
replies = append(replies, v)
}
}
return replies
}
func GetFollowingNewAction(memberId string, limit int, offset int) []*TopicWithAvatar {
var topics []*TopicWithAvatar
err := adapter.Engine.Table("topic").
Join("INNER", "favorites", "favorites.object_id = topic.author").
Where("favorites.member_id = ?", memberId).And("favorites.favorites_type = ?", FollowUser).
Desc("topic.id").
Cols("topic.id, topic.author, topic.node_id, topic.node_name, topic.title, topic.created_time, topic.last_reply_user, topic.last_Reply_time, topic.reply_count, topic.favorite_count, topic.deleted, topic.home_page_top_time, topic.tab_top_time, topic.node_top_time").
Omit("topic.content").
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
for _, topic := range topics {
topic.Avatar = getUserAvatar(topic.Author)
}
return topics
}
func GetNodesFromFavorites(memberId string, limit int, offset int) []*NodeFavoritesRes {
favorites := []*Favorites{}
err := adapter.Engine.Where("member_id = ?", memberId).And("favorites_type = ?", FavorNode).Limit(limit, offset).Find(&favorites)
if err != nil {
panic(err)
}
nodes := []*NodeFavoritesRes{}
for _, v := range favorites {
var temp NodeFavoritesRes
temp.NodeInfo = GetNode(v.ObjectId)
temp.TopicNum = GetNodeTopicNum(v.ObjectId)
nodes = append(nodes, &temp)
}
return nodes
}
func GetNodeFavoritesNum(id string) int {
node := new(Favorites)
total, err := adapter.Engine.Where("favorites_type = ?", FavorNode).And("object_id = ?", id).Count(node)
if err != nil {
panic(err)
}
return int(total)
}
func GetTopicFavoritesNum(id string) int {
topic := new(Favorites)
total, err := adapter.Engine.Where("favorites_type = ?", FavorTopic).And("object_id = ?", id).Count(topic)
if err != nil {
panic(err)
}
return int(total)
}
func GetTopicSubscribeNum(id string) int {
topic := new(Favorites)
total, err := adapter.Engine.Where("favorites_type = ?", SubscribeTopic).And("object_id = ?", id).Count(topic)
if err != nil {
panic(err)
}
return int(total)
}
func GetFollowingNum(id string) int {
member := new(Favorites)
total, err := adapter.Engine.Where("favorites_type = ?", FollowUser).And("member_id = ?", id).Count(member)
if err != nil {
panic(err)
}
return int(total)
}
func GetFavoritesNum(favoritesType string, memberId string) int {
var total int64
var err error
switch favoritesType {
case FavorTopic:
topic := new(Favorites)
total, err = adapter.Engine.Where("favorites_type = ?", FavorTopic).And("member_id = ?", memberId).Count(topic)
if err != nil {
panic(err)
}
break
case FollowUser:
topic := new(Favorites)
total, err = adapter.Engine.Table("topic").Join("INNER", "favorites", "topic.author = favorites.object_id").Where("favorites.member_id = ?", memberId).And("favorites.favorites_type = ?", FollowUser).Count(topic)
if err != nil {
panic(err)
}
break
case FavorNode:
node := new(Favorites)
total, err = adapter.Engine.Where("favorites_type = ?", FavorNode).And("member_id = ?", memberId).Count(node)
if err != nil {
panic(err)
}
break
case SubscribeTopic:
topic := new(Favorites)
total, err = adapter.Engine.Where("favorites_type = ?", SubscribeTopic).And("member_id = ?", memberId).Count(topic)
if err != nil {
panic(err)
}
break
default:
return 0
}
return int(total)
}

151
object/file.go Normal file
View File

@ -0,0 +1,151 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/casdoor/casdoor-go-sdk/casdoorsdk"
type UploadFileRecord struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
FileName string `xorm:"varchar(100)" json:"fileName"`
FilePath string `xorm:"varchar(100)" json:"filePath"`
FileUrl string `xorm:"varchar(100)" json:"fileUrl"`
FileType string `xorm:"varchar(10)" json:"fileType"`
FileExt string `xorm:"varchar(20)" json:"fileExt"`
MemberId string `xorm:"varchar(100) index" json:"memberId"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Size int `xorm:"int" json:"size"`
Views int `xorm:"int" json:"views"`
Desc string `xorm:"varchar(500)" json:"desc"`
Deleted bool `xorm:"bool" json:"-"`
}
func AddFileRecord(record *UploadFileRecord) (bool, int) {
affected, err := adapter.Engine.Insert(record)
if err != nil {
panic(err)
}
return affected != 0, record.Id
}
func GetFile(id int) *UploadFileRecord {
file := UploadFileRecord{Id: id}
existed, err := adapter.Engine.Get(&file)
if err != nil {
panic(err)
}
if existed {
return &file
} else {
return nil
}
}
func GetFiles(memberId string, limit, offset int) []*UploadFileRecord {
records := []*UploadFileRecord{}
err := adapter.Engine.Desc("created_time").Where("member_id = ?", memberId).And("deleted = ?", 0).Limit(limit, offset).Find(&records)
if err != nil {
panic(err)
}
return records
}
//func GetFilesByMember(memberId string) []*UploadFileRecord {
// records := []*UploadFileRecord{}
// err := adapter.Engine.Where("member_id = ?", memberId).Find(&records)
// if err != nil {
// panic(err)
// }
//
// return records
//}
func DeleteFilesByMember(memberId string) bool {
affected, err := adapter.Engine.Where("member_id = ?", memberId).Delete(&UploadFileRecord{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetFilesNum(memberId string) int {
var total int64
var err error
record := new(UploadFileRecord)
total, err = adapter.Engine.Where("member_id = ?", memberId).And("deleted = ?", 0).Count(record)
if err != nil {
panic(err)
}
return int(total)
}
func DeleteFileRecord(id int) bool {
record := new(UploadFileRecord)
record.Deleted = true
affected, err := adapter.Engine.Id(id).Cols("deleted").Update(record)
if err != nil {
panic(err)
}
return affected != 0
}
func FileEditable(user *casdoorsdk.User, author string) bool {
if CheckIsAdmin(user) {
return true
}
if GetUserName(user) != author {
return false
}
return true
}
func AddFileViewsNum(id int) bool {
file := GetFile(id)
if file == nil {
return false
}
file.Views++
affected, err := adapter.Engine.Id(id).Cols("views").Update(file)
if err != nil {
panic(err)
}
return affected != 0
}
func UpdateFileDescribe(id int, fileName, desc string) bool {
file := GetFile(id)
if file == nil {
return false
}
file.Desc = desc
file.FileName = fileName
affected, err := adapter.Engine.Id(id).Cols("desc, file_name").Update(file)
if err != nil {
panic(err)
}
return affected != 0
}

105
object/frontConf.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
type FrontConf struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Value string `xorm:"mediumtext" json:"value"`
Field string `xorm:"varchar(100)" json:"field"`
Tags []string `xorm:"varchar(200)" json:"tags"`
}
var Confs = []*FrontConf{
{Id: "forumName", Value: "Casnode", Field: "visualConf", Tags: nil},
{Id: "logoImage", Value: "https://cdn.casbin.com/forum/static/img/logo.png", Field: "visualConf", Tags: nil},
{Id: "footerLogoImage", Value: "https://cdn.casbin.com/forum/static/img/logo-footer.png", Field: "visualConf", Tags: nil},
{Id: "footerLogoUrl", Value: "https://www.digitalocean.com/", Field: "visualConf", Tags: nil},
{Id: "signinBoxStrong", Value: "Casbin = way to authorization", Field: "visualConf", Tags: nil},
{Id: "signinBoxSpan", Value: "A place for Casbin developers and users", Field: "visualConf", Tags: nil},
{Id: "footerDeclaration", Value: "World is powered by code", Field: "visualConf", Tags: nil},
{Id: "footerAdvise", Value: "♥ Do have faith in what you're doing.", Field: "visualConf", Tags: nil},
{Id: "faq", Value: "Not yet", Field: "", Tags: nil},
{Id: "mission", Value: "Not yet", Field: "", Tags: nil},
{Id: "advertise", Value: "Not yet", Field: "", Tags: nil},
{Id: "thanks", Value: "Not yet", Field: "", Tags: nil},
}
func InitFrontConf() {
var confs []*FrontConf
err := adapter.Engine.Find(&confs)
if err != nil {
panic(err)
}
if len(confs) > 0 {
return
}
confs = Confs
_, err = adapter.Engine.Insert(&confs)
if err != nil {
panic(err)
}
}
func GetFrontConfById(id string) *FrontConf {
var confs []*FrontConf
err := adapter.Engine.Where("id = ?", id).Find(&confs)
if err != nil {
panic(err)
}
if len(confs) == 0 {
return nil
} else {
return confs[0]
}
}
func GetFrontConfsByField(field string) []*FrontConf {
var confs []*FrontConf
err := adapter.Engine.Where("field = ?", field).Find(&confs)
if err != nil {
panic(err)
}
return confs
}
func UpdateFrontConfs(confs []*FrontConf) bool {
var err error
for _, conf := range confs {
_, err = adapter.Engine.Where("id = ?", conf.Id).Update(conf)
if err != nil {
panic(err)
}
}
return true
}
func UpdateFrontConfById(id string, value string, tags []string) (int64, error) {
return adapter.Engine.Id(id).Cols("value", "tags").Update(&FrontConf{Value: value, Tags: tags})
}
func UpdateFrontConfsByField(confs []*FrontConf, field string) error {
for _, conf := range confs {
if conf.Field == field {
_, err := adapter.Engine.Id(conf.Id).Cols("value").Update(conf)
if err != nil {
return err
}
}
}
return nil
}

494
object/gitter.go Normal file
View File

@ -0,0 +1,494 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"runtime"
"strconv"
"sync"
"time"
"github.com/astaxie/beego/logs"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/sromku/go-gitter"
)
const (
topicDuration = "4" // Hours
apiLIMIT = 10 // request frequency
)
type topicGitter struct {
Topic Topic
Massages []gitter.Message
MemberMsgMap map[string]int
}
var (
roomSyncMsgHeadMap = map[string]string{}
roomSyncMsgTailMap = map[string]string{}
lastMsgMap = map[string]gitter.Message{}
lastTopicMap = map[string]topicGitter{}
currentTopicMap = map[string]topicGitter{}
)
func AutoSyncGitter() {
if AutoSyncPeriodSecond < 30 {
return
}
SyncAllGitterRooms()
for {
time.Sleep(time.Duration(AutoSyncPeriodSecond) * time.Second)
SyncAllGitterRooms()
}
}
func SyncAllGitterRooms() {
fmt.Println("Sync from gitter room started...")
var nodes []Node
err := adapter.Engine.Find(&nodes)
if err != nil {
panic(err)
}
for _, node := range nodes {
node.SyncGitter()
}
}
func (n Node) SyncGitter() {
if n.GitterRoomURL == "" || n.GitterApiToken == "" {
return
}
defer func() {
if err := recover(); err != nil {
handleErr(err.(error))
}
}()
// Get your own token At https://developer.gitter.im/
api := gitter.New(n.GitterApiToken)
// get room id by room url
rooms, err := api.GetRooms()
if err != nil {
panic(err)
}
fmt.Println("gitter room urls:", n.GitterRoomURL)
room := gitter.Room{}
for _, v := range rooms { // find RoomId by url
if "https://gitter.im/"+v.URI == n.GitterRoomURL {
room = v
break
}
}
if room.Name == "" {
panic(errors.New("room is not exist"))
}
topics := n.GetAllTopicsByNode()
topicNum := len(topics)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
messages := []gitter.Message{}
// get sync index, it is the last sync message id
headIdx, ok := roomSyncMsgHeadMap[room.ID]
if !ok { // get all msg if idx is not exist
for _, topic := range topics {
if topic.GitterMessageId != "" {
// get reply
replies := GetRepliesOfTopic(topic.Id)
// get sync msg idx
num := len(replies)
if num == 0 {
headIdx = topic.GitterMessageId
break
}
flag := false
for i := num - 1; i >= 0; i-- {
if replies[i].GitterMessageId != "" {
headIdx = replies[i].GitterMessageId
flag = true
break
}
}
if flag {
break
}
}
}
}
// the api limits the number of messages to 50
messages, err = api.GetMessages(room.ID, &gitter.Pagination{
AfterID: headIdx,
})
if err != nil {
panic(err)
}
if len(messages) == 0 {
roomSyncMsgHeadMap[room.ID] = headIdx
return
}
for i := 0; i < apiLIMIT; i++ { // restrict request frequency
msgs, err := api.GetMessages(room.ID, &gitter.Pagination{
AfterID: messages[len(messages)-1].ID,
})
if err != nil {
panic(err)
}
if len(msgs) == 0 {
break
}
messages = append(messages, msgs...)
}
fmt.Printf("sync msg for room(msgNum:%d): %s\n", len(messages), room.Name)
createTopicWithMessages(messages, room, n, topics, true)
}()
// tail
go func() {
defer wg.Done()
messages := []gitter.Message{}
tailIdx, ok := roomSyncMsgTailMap[room.ID]
if !ok {
for i := topicNum - 1; i >= 0; i-- {
topic := topics[i]
if topic.GitterMessageId != "" {
tailIdx = topic.GitterMessageId
break
}
}
}
t := time.Time{}
tExist := true // if t is not exist, sync all msg
if n.GitterSyncFromTime == "" {
tExist = false
} else {
t, err = time.Parse(time.RFC3339, n.GitterSyncFromTime)
if err != nil {
panic(err)
}
}
if tailIdx != "" {
tailMsg, err := api.GetMessage(room.ID, tailIdx)
if err != nil {
panic(err)
}
if tExist {
if tailMsg.Sent.Before(t) { // if msg is before the start time, end sync tail
return
}
}
} else {
messages, err = api.GetMessages(room.ID, nil)
if len(messages) != 0 {
tailIdx = messages[0].ID
}
}
messages, err = api.GetMessages(room.ID, &gitter.Pagination{
BeforeID: tailIdx,
})
if err != nil {
panic(err)
}
if len(messages) == 0 {
roomSyncMsgTailMap[room.ID] = tailIdx
return
}
for i := 0; i < apiLIMIT; i++ { // restrict request frequency
msgs, err := api.GetMessages(room.ID, &gitter.Pagination{
BeforeID: messages[0].ID,
})
if err != nil {
panic(err)
}
num := len(msgs)
if num == 0 {
break
}
// if msg is before the start time, end sync tail
if tExist {
if msgs[0].Sent.Before(t) {
for i := num - 1; i > 0; i-- {
if msgs[i].Sent.Before(t) {
if i == num-1 {
msgs = []gitter.Message{}
} else {
msgs = msgs[i+1:]
}
break
}
}
messages = append(msgs, messages...)
break
}
}
messages = append(msgs, messages...)
}
fmt.Printf("sync msg for room(msgNum:%d): %s\n", len(messages), room.Name)
createTopicWithMessages(messages, room, n, topics, false)
}()
wg.Wait()
}
// main create topic func
func createTopicWithMessages(messages []gitter.Message, room gitter.Room, node Node, topics []Topic, asc bool) {
GetTopicExist := func(topicTitle string) Topic {
for _, topic := range topics {
if topic.Title == topicTitle {
return topic
}
}
return Topic{}
}
// initialize value
lastMsg, ok := lastMsgMap[room.ID]
if !ok {
lastMsg = gitter.Message{}
}
lastTopic := lastTopicMap[room.ID]
if !ok {
lastTopic = topicGitter{MemberMsgMap: map[string]int{}}
}
currentTopic, ok := currentTopicMap[room.ID]
if !ok {
currentTopic = topicGitter{MemberMsgMap: map[string]int{}}
}
for _, msg := range messages {
func() {
defer func() {
if err := recover(); err != nil {
handleErr(err.(error))
}
}()
// create if user is not exist
user, err := casdoorsdk.GetUser(msg.From.Username)
// fmt.Println("user:", user)
if err != nil {
panic(err)
}
if user.Id == "" { // add user
avatar := getGitterAvatarUrl(msg.From.Username, msg.From.AvatarURLMedium) // if error, avatar will be ""
newUser := casdoorsdk.User{
Name: msg.From.Username,
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
DisplayName: msg.From.DisplayName,
Avatar: avatar,
SignupApplication: CasdoorApplication,
}
fmt.Println("add user: ", newUser.Name)
_, err := casdoorsdk.AddUser(&newUser)
if err != nil {
panic(err)
}
}
mentioned := false // if @user
for _, mention := range msg.Mentions {
if mention.ScreenName == lastMsg.From.Username {
mentioned = true
break
}
}
// if @user and lastMsg is not @user, then create topic
// if duration is more than 4 hour, then create topic
d := msg.Sent.Sub(lastMsg.Sent)
dur, err := strconv.Atoi(topicDuration)
if err != nil {
panic(err)
}
if d > time.Hour*time.Duration(dur) && !mentioned { // if dur > `TopicDuration` and not @user last replied
tmpStr := []rune(msg.Text)
if len(tmpStr) > 200 { // limit length
tmpStr = tmpStr[:200]
}
title := string(tmpStr)
topic := GetTopicExist(title)
if topic.Id == 0 { // not exist
// add topic
topic = Topic{
Author: msg.From.Username,
NodeId: node.Id,
NodeName: node.Name,
TabId: node.TabId,
Title: title,
CreatedTime: util.Time2String(msg.Sent),
LastReplyTime: util.Time2String(msg.Sent),
Tags: service.Finalword(msg.Text),
EditorType: "markdown",
Content: msg.Text,
GitterMessageId: msg.ID,
}
_, topicID := AddTopic(&topic)
topic.Id = topicID
}
// deep copy
data, _ := json.Marshal(currentTopic)
_ = json.Unmarshal(data, &lastTopic)
lastTopicMap[room.ID] = lastTopic
// new currentTopic
currentTopic = topicGitter{Topic: topic, MemberMsgMap: map[string]int{}}
currentTopic.Massages = append(currentTopic.Massages, msg)
currentTopic.MemberMsgMap[msg.From.Username]++
currentTopicMap[room.ID] = currentTopic
} else {
// add reply to lastTopic
reply := Reply{
Author: msg.From.Username,
TopicId: currentTopic.Topic.Id,
CreatedTime: util.Time2String(msg.Sent),
Tags: service.Finalword(msg.Text),
EditorType: "markdown",
Content: msg.Text,
GitterMessageId: msg.ID,
}
_, _ = AddReply(&reply)
ChangeTopicReplyCount(reply.TopicId, 1)
ChangeTopicLastReplyUser(currentTopic.Topic.Id, msg.From.Username, util.Time2String(msg.Sent))
currentTopic.Massages = append(currentTopic.Massages, msg)
currentTopic.MemberMsgMap[msg.From.Username]++
currentTopicMap[room.ID] = currentTopic
}
// deep copy
data, _ := json.Marshal(msg)
_ = json.Unmarshal(data, &lastMsg)
// add index to sync message
if asc {
roomSyncMsgHeadMap[room.ID] = msg.ID
} else {
roomSyncMsgTailMap[room.ID] = messages[0].ID
}
}()
}
}
func handleErr(err error) {
var stack string
logs.Critical("Handler crashed with error:", err)
for i := 1; ; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
logs.Critical(fmt.Sprintf("%s:%d", file, line))
stack = stack + fmt.Sprintln(fmt.Sprintf("%s:%d", file, line))
}
}
func getGitterAvatarUrl(username string, avatar string) string {
var fileBytes []byte
var fileExt string
var err error
times := 0
for {
fileBytes, _, err = downloadFile(avatar)
if err != nil {
times += 1
fmt.Printf("[%d]: downloadFile() error: %s, times = %d\n", username, err.Error(), times)
if times >= 10 {
panic(err)
}
} else {
break
}
}
fileExt = ".png"
avatarUrl, err := uploadGitterAvatar(username, fileBytes, fileExt)
if err != nil {
avatarUrl = ""
}
return avatarUrl
}
func uploadGitterAvatar(username string, fileBytes []byte, fileExt string) (string, error) {
username = url.QueryEscape(username)
memberId := fmt.Sprintf("%s/%s", CasdoorOrganization, username)
fileUrl, err := service.UploadFileToStorageSafe(memberId, "avatar", "uploadGitterAvatar", fmt.Sprintf("avatar/%s%s", memberId, fileExt), fileBytes, "", "")
return fileUrl, err
}
func downloadFile(url string) ([]byte, string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, "", err
}
resp, err := client.Do(req)
if err != nil {
return nil, "", err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
return
}
}(resp.Body)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, "", err
}
newUrl := resp.Request.URL.String()
return bs, newUrl, nil
}

63
object/gitter_test.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"testing"
"github.com/issue9/assert"
"github.com/sromku/go-gitter"
)
func TestRemoveSyncGitterData(t *testing.T) {
InitConfig()
InitAdapter()
// delete all sync gitter data
var nodes []Node
err := adapter.Engine.Find(&nodes)
if err != nil {
panic(err)
}
for _, node := range nodes {
if node.GitterRoomURL == "" || node.GitterApiToken == "" {
continue
}
api := gitter.New(node.GitterApiToken)
rooms, err := api.GetRooms()
if err != nil {
panic(err)
}
url := node.GitterRoomURL
room := gitter.Room{}
for _, v := range rooms { // find RoomId by url
if "https://gitter.im/"+v.URI == url {
room = v
break
}
}
assert.NotEqual(t, room.Name, "")
adapter.Engine.ShowSQL(true)
_, err = adapter.Engine.
Query("DELETE t.*,r.* FROM topic as t LEFT JOIN reply as r ON t.id = r.topic_id WHERE t.gitter_message_id is not null AND t.node_id = ?", node.Id)
if err != nil {
panic(err)
}
fmt.Printf("INFO: delete sync gitter data of room: %s\n", room.Name)
}
}

108
object/hot.go Normal file
View File

@ -0,0 +1,108 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
// RecordType: 1 means node hit record
type BrowseRecord struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
MemberId string `xorm:"varchar(100)" json:"memberId"`
RecordType int `xorm:"int" json:"recordType"`
ObjectId string `xorm:"varchar(100) index" json:"objectId"`
CreatedTime string `xorm:"varchar(40) index" json:"createdTime"`
Expired bool `xorm:"bool" json:"expired"`
}
func GetBrowseRecordNum(recordType int, objectId string) int {
record := new(BrowseRecord)
total, err := adapter.Engine.Where("object_id = ?", objectId).And("record_type = ?", recordType).And("expired = ?", false).Count(record)
if err != nil {
panic(err)
}
return int(total)
}
func DeletedExpiredData(recordType int, date string) bool {
affected, err := adapter.Engine.Where("record_type = ?", recordType).And("date < ?", date).Delete(&BrowseRecord{})
if err != nil {
panic(err)
}
return affected != 0
}
func AddBrowseRecordNum(record *BrowseRecord) bool {
affected, err := adapter.Engine.Insert(record)
if err != nil {
panic(err)
}
return affected != 0
}
func ChangeExpiredDataStatus(recordType int, date string) int {
var res int
record := new(BrowseRecord)
record.Expired = true
affected, err := adapter.Engine.Where("record_type = ?", recordType).And("expired = ?", 0).And("created_time < ?", date).Cols("expired").Update(record)
res += int(affected)
if err != nil {
panic(err)
}
return res
}
func GetLastRecordId() int {
record := new(BrowseRecord)
_, err := adapter.Engine.Desc("id").Cols("id").Limit(1).Get(record)
if err != nil {
panic(err)
}
res := record.Id
return res
}
func UpdateHotNode(last int) int {
var record []*BrowseRecord
err := adapter.Engine.Table("browse_record").Where("id > ?", last).And("record_type = ?", 1).GroupBy("object_id").Find(&record)
if err != nil {
panic(err)
}
for _, v := range record {
hot := GetBrowseRecordNum(1, v.ObjectId)
UpdateNodeHotInfo(v.ObjectId, hot)
}
return len(record)
}
func UpdateHotTopic(last int) int {
var record []*BrowseRecord
err := adapter.Engine.Table("browse_record").Where("id > ? ", last).And("record_type = ?", 2).GroupBy("object_id").Find(&record)
if err != nil {
panic(err)
}
for _, v := range record {
hot := GetBrowseRecordNum(2, v.ObjectId)
UpdateTopicHotInfo(v.ObjectId, hot)
}
return len(record)
}

165
object/mailing_list.go Normal file
View File

@ -0,0 +1,165 @@
package object
import (
"fmt"
"strings"
"time"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
crawler "github.com/casbin/google-groups-crawler"
"github.com/gomarkdown/markdown"
)
func (n Node) AddTopicToMailingList(title, content, author string) {
if len(n.MailingList) == 0 {
return
}
content = string(markdown.ToHTML([]byte(content), nil, nil))
_ = service.SendEmail(title, content, author, n.MailingList)
}
func (n Node) SyncFromGoogleGroup() {
if !strings.Contains(n.MailingList, "@googlegroups.com") {
return
}
topicTitles := n.GetAllTopicTitlesOfNode()
isInTopicList := func(topicTitle string) bool {
for _, title := range topicTitles {
if title == topicTitle {
return true
}
}
return false
}
group := crawler.NewGoogleGroup(n.MailingList, n.GoogleGroupCookie)
conversations := group.GetAllConversations(*httpClient)
for _, conv := range conversations {
messages := conv.GetAllMessages(*httpClient, true)
if len(messages) < 1 {
fmt.Printf("Google Groups Crawler: Getting messages from Google Group: %s for node: %s failed, please check your cookie.\n", group.GroupName, n.Id)
break
}
var newTopic Topic
AuthorMember, err := AddMemberByNameAndEmailIfNotExist(messages[0].Author, messages[0].AuthorEmail)
if err != nil {
panic(err)
}
if AuthorMember == nil {
continue
}
for msgIndex, msgItem := range messages {
if msgItem.Files != nil {
for _, msgFileItem := range msgItem.Files {
messages[msgIndex].Content = fmt.Sprintf("%s<br>[%s](%s)", messages[msgIndex].Content, msgFileItem.FileName, msgFileItem.Url)
}
}
}
if !isInTopicList(conv.Title) {
newTopic = Topic{
Author: AuthorMember.Id,
NodeId: n.Id,
NodeName: n.Name,
TabId: n.TabId,
Title: conv.Title,
Content: FilterUnsafeHTML(messages[0].Content),
CreatedTime: util.GetTimeFromTimestamp(int64(conv.Time)),
LastReplyTime: util.GetTimeFromTimestamp(int64(conv.Time)),
EditorType: "richtext",
}
AddTopic(&newTopic)
} else {
var topics []Topic
err := adapter.Engine.Where("title = ? and deleted = 0", conv.Title).Find(&topics)
if err != nil {
panic(err)
}
if len(topics) == 0 {
continue
}
for _, t := range topics {
if conv.Title == t.Title {
newTopic = t
break
}
}
}
replies := newTopic.GetAllRepliesOfTopic()
isInReplies := func(replyStr string) bool {
for _, c := range replies {
if c == replyStr {
return true
}
}
return false
}
for _, msg := range messages[1:] {
msg.Content = FilterUnsafeHTML(msg.Content)
AuthorMember, err = AddMemberByNameAndEmailIfNotExist(msg.Author, msg.AuthorEmail)
if err != nil {
panic(err)
}
if AuthorMember == nil {
continue
}
if isInReplies(msg.Content) {
continue
}
newReply := Reply{
Author: AuthorMember.Id,
TopicId: newTopic.Id,
EditorType: "richtext",
Content: msg.Content,
CreatedTime: util.GetTimeFromTimestamp(int64(msg.Time)),
}
AddReply(&newReply)
newTopic.LastReplyTime = util.GetTimeFromTimestamp(int64(msg.Time))
newTopic.LastReplyUser = AuthorMember.Id
}
UpdateTopic(newTopic.Id, &newTopic)
}
}
func AutoSyncGoogleGroup() {
if AutoSyncPeriodSecond < 30 {
return
}
for {
time.Sleep(time.Duration(AutoSyncPeriodSecond) * time.Second)
SyncAllNodeFromGoogleGroup()
}
}
func SyncAllNodeFromGoogleGroup() {
if AutoSyncPeriodSecond < 30 {
return
}
fmt.Println("Sync from google group started...")
var nodes []Node
err := adapter.Engine.Find(&nodes)
if err != nil {
panic(err)
}
for _, node := range nodes {
node.SyncFromGoogleGroup()
}
}
func (r Reply) AddReplyToMailingList() {
targetTopic := GetTopic(r.TopicId)
targetNode := GetNode(targetTopic.NodeId)
if len(targetNode.MailingList) == 0 {
return
}
if r.EditorType == "markdown" {
r.Content = string(markdown.ToHTML([]byte(r.Content), nil, nil))
}
mailTitle := fmt.Sprintf("Re: %s", targetTopic.Title)
_ = service.SendEmail(mailTitle, r.Content, r.Author, targetNode.MailingList)
}

207
object/member.go Normal file
View File

@ -0,0 +1,207 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strconv"
"strings"
"github.com/casbin/casnode/casdoor"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
func GetRankingRich() ([]*casdoorsdk.User, error) {
return casdoor.GetSortedUsers("score", 25), nil
}
func GetRankingPlayer() ([]*casdoorsdk.User, error) {
return casdoor.GetSortedUsers("karma", 25), nil
}
func GetUser(id string) *casdoorsdk.User {
user := casdoor.GetUser(id)
return user
}
func GetUsers() []*casdoorsdk.User {
users := casdoor.GetUsers()
return users
}
func GetMemberNum() int {
return casdoor.GetUserCount()
}
func UpdateMemberEditorType(user *casdoorsdk.User, editorType string) (bool, error) {
if user == nil {
return false, fmt.Errorf("user is nil")
}
SetUserField(user, "editorType", editorType)
return casdoorsdk.UpdateUserForColumns(user, []string{"properties"})
}
func GetMemberEditorType(user *casdoorsdk.User) string {
return GetUserField(user, "editorType")
}
func UpdateMemberLanguage(user *casdoorsdk.User, language string) (bool, error) {
SetUserField(user, "language", language)
return casdoorsdk.UpdateUserForColumns(user, []string{"properties"})
}
func GetMemberLanguage(user *casdoorsdk.User) string {
return GetUserField(user, "language")
}
// GetMemberEmailReminder return member's email reminder status, and his email address.
func GetMemberEmailReminder(id string) (bool, string) {
user := GetUser(id)
if user == nil {
return false, ""
}
return true, user.Email
}
func GetUserByEmail(email string) *casdoorsdk.User {
return casdoor.GetUserByEmail(email)
}
func GetMemberCheckinDate(user *casdoorsdk.User) string {
return GetUserField(user, "checkinDate")
}
func UpdateMemberCheckinDate(user *casdoorsdk.User, checkinDate string) (bool, error) {
SetUserField(user, "checkinDate", checkinDate)
return casdoorsdk.UpdateUserForColumns(user, []string{"properties"})
}
func GetUserName(user *casdoorsdk.User) string {
if user == nil {
return ""
}
return user.Name
}
func CheckIsAdmin(user *casdoorsdk.User) bool {
if user == nil {
return false
}
return user.IsAdmin
}
func GetMemberFileQuota(user *casdoorsdk.User) int {
if user == nil {
return 0
}
return GetUserFieldInt(user, "fileQuota")
}
// UpdateMemberOnlineStatus updates member's online information.
func UpdateMemberOnlineStatus(user *casdoorsdk.User, isOnline bool, lastActionDate string) (bool, error) {
if user == nil {
return false, fmt.Errorf("user is nil")
}
user.IsOnline = isOnline
SetUserField(user, "lastActionDate", lastActionDate)
return casdoorsdk.UpdateUserForColumns(user, []string{"is_online", "properties"})
}
func GetOnlineUserCount() int {
return casdoor.GetOnlineUserCount()
}
type UpdateListItem struct {
Table string
Attribute string
}
func AddMemberByNameAndEmailIfNotExist(username, email string) (*casdoorsdk.User, error) {
username = strings.ReplaceAll(username, " ", "")
if username == "" {
return nil, fmt.Errorf("username is empty")
}
email = strings.ReplaceAll(email, " ", "")
if email == "" {
return nil, fmt.Errorf("email is empty")
}
user, err := casdoorsdk.GetUser(username)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}
username = strings.Split(email, "@")[0]
user, err = casdoorsdk.GetUser(username)
if err != nil {
return nil, err
}
if user != nil {
return user, nil
}
newUser := GetUserByEmail(email)
if newUser == nil {
properties := map[string]string{}
properties["emailVerifiedTime"] = util.GetCurrentTime()
properties["fileQuota"] = strconv.Itoa(DefaultUploadFileQuota)
properties["renameQuota"] = strconv.Itoa(DefaultRenameQuota)
newUser = &casdoorsdk.User{
Name: username,
CreatedTime: util.GetCurrentTime(),
UpdatedTime: util.GetCurrentTime(),
Id: "",
Type: "",
Password: "",
DisplayName: "",
Avatar: "",
Email: email,
Phone: "",
Location: "",
Address: nil,
Affiliation: "",
Title: "",
Homepage: "",
Tag: "",
Score: getInitScore(),
Ranking: GetMemberNum() + 1,
IsOnline: false,
IsAdmin: false,
IsGlobalAdmin: false,
IsForbidden: false,
SignupApplication: CasdoorApplication,
Properties: properties,
}
_, err = casdoorsdk.AddUser(newUser)
if err != nil {
return newUser, err
}
}
return newUser, nil
}

357
object/node.go Normal file
View File

@ -0,0 +1,357 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"sync"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
type Node struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Name string `xorm:"varchar(100)" json:"name"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Desc string `xorm:"mediumtext" json:"desc"`
Extra string `xorm:"mediumtext" json:"extra"`
Image string `xorm:"varchar(200)" json:"image"`
BackgroundImage string `xorm:"varchar(200)" json:"backgroundImage"`
HeaderImage string `xorm:"varchar(200)" json:"headerImage"`
BackgroundColor string `xorm:"varchar(20)" json:"backgroundColor"`
BackgroundRepeat string `xorm:"varchar(20)" json:"backgroundRepeat"`
TabId string `xorm:"varchar(100)" json:"tab"`
ParentNode string `xorm:"varchar(200)" json:"parentNode"`
PlaneId string `xorm:"varchar(50)" json:"planeId"`
Sorter int `json:"sorter"`
Ranking int `json:"ranking"`
Hot int `json:"hot"`
Moderators []string `xorm:"varchar(200)" json:"moderators"`
MailingList string `xorm:"varchar(100)" json:"mailingList"`
GoogleGroupCookie string `xorm:"varchar(1500)" json:"googleGroupCookie"`
GitterApiToken string `xorm:"varchar(200)" json:"gitterApiToken"`
GitterRoomURL string `xorm:"varchar(200)" json:"gitterRoomUrl"`
GitterSyncFromTime string `xorm:"varchar(40)" json:"gitterSyncFromTime"`
IsHidden bool `xorm:"bool" json:"isHidden"`
}
func GetNodes() []*Node {
nodes := []*Node{}
err := adapter.Engine.Desc("sorter").Find(&nodes)
if err != nil {
panic(err)
}
return nodes
}
func GetNode(id string) *Node {
if id == "" {
return nil
}
node := Node{Id: id}
existed, err := adapter.Engine.Get(&node)
if err != nil {
panic(err)
}
if existed {
return &node
} else {
return nil
}
}
func UpdateNode(id string, node *Node) bool {
if GetNode(id) == nil {
return false
}
_, err := adapter.Engine.Id(id).AllCols().Update(node)
if err != nil {
panic(err)
}
isHidden := "0"
if node.IsHidden {
isHidden = "1"
}
_, err = adapter.Engine.Query("update topic set is_hidden = ? where node_id = ?", isHidden, node.Id)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
func AddNode(node *Node) bool {
affected, err := adapter.Engine.Insert(node)
if err != nil {
panic(err)
}
return affected != 0
}
func AddNodes(nodes []*Node) bool {
affected, err := adapter.Engine.Insert(nodes)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteNode(id string) bool {
affected, err := adapter.Engine.Id(id).Delete(&Node{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetNodesNum() int {
node := new(Node)
total, err := adapter.Engine.Count(node)
if err != nil {
panic(err)
}
return int(total)
}
func GetNodeTopicNum(id string) int {
topic := new(Topic)
total, err := adapter.Engine.Where("node_id = ?", id).And("deleted = ?", 0).Count(topic)
if err != nil {
panic(err)
}
return int(total)
}
func GetNodeFromTab(tab string) []*Node {
nodes := []*Node{}
err := adapter.Engine.Where("tab_id = ?", tab).Desc("sorter").Find(&nodes)
if err != nil {
panic(err)
}
return nodes
}
func GetNodeFromPlane(plane string) []*Node {
nodes := []*Node{}
err := adapter.Engine.Where("plane_id = ?", plane).Cols("id, name").Desc("sorter").Find(&nodes)
if err != nil {
panic(err)
}
return nodes
}
func GetNodeRelation(id string) *NodeRelation {
node := new(Node)
parentNode := new(Node)
relatedNode := []*Node{}
childNode := []*Node{}
_, err := adapter.Engine.Id(id).Cols("parent_node").Get(node)
if err != nil {
panic(err)
}
var wg sync.WaitGroup
wg.Add(3)
go func() {
defer wg.Done()
_, err = adapter.Engine.Id(node.ParentNode).Get(parentNode)
if err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
err = adapter.Engine.Table("node").Where("parent_node = ?", node.ParentNode).And("id != ?", node.ParentNode).Find(&relatedNode)
if err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
err = adapter.Engine.Table("node").Where("parent_node = ?", id).And("id != ?", node.ParentNode).Find(&childNode)
if err != nil {
panic(err)
}
}()
wg.Wait()
res := &NodeRelation{
ParentNode: parentNode,
RelatedNode: relatedNode,
ChildNode: childNode,
}
return res
}
func GetNodeNavigation() []*NodeNavigationResponse {
tabs := GetAllTabs()
nodes := GetNodes()
nodesMap := map[string][]*Node{}
for _, node := range nodes {
if _, ok := nodesMap[node.TabId]; !ok {
nodesMap[node.TabId] = []*Node{}
}
nodesMap[node.TabId] = append(nodesMap[node.TabId], node)
}
res := []*NodeNavigationResponse{}
for _, tab := range tabs {
temp := NodeNavigationResponse{
Tab: tab,
Nodes: nodesMap[tab.Id],
}
res = append(res, &temp)
}
return res
}
func GetLatestNode(limit int) []*Node {
nodes := []*Node{}
err := adapter.Engine.Asc("created_time").Limit(limit).Find(&nodes)
if err != nil {
panic(err)
}
return nodes
}
func GetHotNode(limit int) []*Node {
nodes := []*Node{}
err := adapter.Engine.Desc("hot").Limit(limit).Find(&nodes)
if err != nil {
panic(err)
}
return nodes
}
func UpdateNodeHotInfo(nodeId string, hot int) bool {
node := new(Node)
node.Hot = hot
affected, err := adapter.Engine.Id(nodeId).Cols("hot").Update(node)
if err != nil {
panic(err)
}
return affected != 0
}
func GetNodeModerators(id string) []string {
node := Node{Id: id}
existed, err := adapter.Engine.Cols("moderators").Get(&node)
if err != nil {
panic(err)
}
if existed {
return node.Moderators
} else {
return nil
}
}
func CheckNodeModerator(user *casdoorsdk.User, nodeId string) bool {
node := Node{Id: nodeId}
existed, err := adapter.Engine.Cols("moderators").Get(&node)
if err != nil {
panic(err)
}
if !existed || len(node.Moderators) == 0 {
return false
}
for _, v := range node.Moderators {
if v == GetUserName(user) {
return true
}
}
return false
}
func AddNodeModerators(memberId, nodeId string) bool {
node := new(Node)
moderators := GetNodeModerators(nodeId)
for _, v := range moderators {
if v == memberId {
return false
}
}
node.Moderators = append(moderators, memberId)
affected, err := adapter.Engine.Id(nodeId).Cols("moderators").Update(node)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteNodeModerators(memberId, nodeId string) bool {
node := new(Node)
moderators := GetNodeModerators(nodeId)
for i, v := range moderators {
if v == memberId {
moderators = append(moderators[:i], moderators[i+1:]...)
break
}
}
node.Moderators = moderators
affected, err := adapter.Engine.Id(nodeId).Cols("moderators").Update(node)
if err != nil {
panic(err)
}
return affected != 0
}
func (n Node) GetAllTopicTitlesOfNode() []string {
var topics []Topic
var ret []string
err := adapter.Engine.Where("node_id = ? and deleted = 0", n.Id).Find(&topics)
if err != nil {
panic(err)
}
for _, topic := range topics {
ret = append(ret, topic.Title)
}
return ret
}
func (n Node) GetAllTopicsByNode() []Topic {
var topics []Topic
err := adapter.Engine.Where("node_id = ? and deleted = 0", n.Id).Desc("created_time").Find(&topics)
if err != nil {
panic(err)
}
return topics
}

320
object/notification.go Normal file
View File

@ -0,0 +1,320 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"regexp"
"strconv"
"sync"
"github.com/astaxie/beego"
"github.com/casbin/casnode/service"
"github.com/casbin/casnode/util"
)
// NotificationType 1-6 means: reply(topic), mentioned(reply), mentioned(topic), favorite(topic), thanks(topic), thanks(reply)
// Status 1-3 means: unread, have read, deleted
type Notification struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
NotificationType int `xorm:"int index" json:"notificationType"`
ObjectId int `xorm:"int index" json:"objectId"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
SenderId string `xorm:"varchar(100)" json:"senderId"`
ReceiverId string `xorm:"varchar(100) index" json:"receiverId"`
Status int `xorm:"tinyint" json:"-"`
// Deleted bool `xorm:"bool" json:"-"`
}
func AddNotification(notification *Notification) bool {
affected, err := adapter.Engine.Insert(notification)
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteNotification(id string) bool {
notification := new(Notification)
notification.Status = 3
affected, err := adapter.Engine.Id(id).Update(notification)
if err != nil {
panic(err)
}
return affected != 0
}
func GetNotificationCount() int {
count, err := adapter.Engine.Count(&Notification{})
if err != nil {
panic(err)
}
return int(count)
}
func GetNotifications(memberId string, limit int, offset int) []*NotificationResponse {
notifications := []*NotificationResponse{}
err := adapter.Engine.Table("notification").
Where("notification.receiver_id = ?", memberId).And("notification.status != ?", 3).
Desc("notification.created_time").
Cols("notification.*").
Limit(limit, offset).Find(&notifications)
if err != nil {
panic(err)
}
for _, notification := range notifications {
notification.Avatar = getUserAvatar(notification.Notification.SenderId)
}
var wg sync.WaitGroup
errChan := make(chan error, 10)
res := make([]*NotificationResponse, len(notifications))
for k, v := range notifications {
wg.Add(1)
go func(k int, v *NotificationResponse) {
defer wg.Done()
switch v.NotificationType {
case 1:
fallthrough
case 2:
fallthrough
case 6:
replyInfo := GetReply(v.ObjectId)
if replyInfo == nil || replyInfo.Deleted {
break
}
v.Title = GetReplyTopicTitle(replyInfo.TopicId)
v.Content = replyInfo.Content
v.ObjectId = replyInfo.TopicId
case 3:
v.Title = GetTopicTitle(v.ObjectId)
case 4:
v.Title = GetTopicTitle(v.ObjectId)
case 5:
v.Title = GetTopicTitle(v.ObjectId)
}
res[k] = v
}(k, v)
}
wg.Wait()
close(errChan)
if len(errChan) != 0 {
for v := range errChan {
panic(v)
}
}
return res
}
func GetNotificationNum(memberId string) int {
var total int64
var err error
notification := new(Notification)
total, err = adapter.Engine.Where("receiver_id = ?", memberId).And("status != ?", 3).Count(notification)
if err != nil {
panic(err)
}
return int(total)
}
func GetUnreadNotificationNum(memberId string) int {
var total int64
var err error
notification := new(Notification)
total, err = adapter.Engine.Where("receiver_id = ?", memberId).And("status = ?", 1).Count(notification)
if err != nil {
panic(err)
}
return int(total)
}
/*
func GetNotificationId() int {
num := GetNotificationCount()
res := num + 1
return res
}
*/
func UpdateReadStatus(id string) bool {
notification := new(Notification)
notification.Status = 2
affected, err := adapter.Engine.Where("receiver_id = ?", id).Cols("status").Update(notification)
if err != nil {
panic(err)
}
return affected != 0
}
func AddReplyNotification(senderId, content string, objectId, topicId int) {
memberMap := make(map[string]bool)
topicInfo := GetTopicBasicInfo(topicId)
receiverId := topicInfo.Author
memberMap[receiverId] = true
reg := regexp.MustCompile("@(.*?)[ \n\t]")
reg2 := regexp.MustCompile("@([^ \n\t]*?)[^ \n\t]$")
regResult := reg.FindAllStringSubmatch(content, -1)
regResult2 := reg2.FindAllStringSubmatch(content, -1)
for _, v := range regResult {
if senderId != v[1] && !memberMap[v[1]] {
memberMap[v[1]] = true
AddMemberFavorites(v[1], "subscribe_topic", strconv.Itoa(topicId))
}
}
for _, v := range regResult2 {
v[1] += content[len(content)-1:]
if senderId != v[1] && !memberMap[v[1]] {
memberMap[v[1]] = true
AddMemberFavorites(v[1], "subscribe_topic", strconv.Itoa(topicId))
}
}
subscribeUsers := GetMembersFromFavorites(strconv.Itoa(topicId), SubscribeTopic)
for _, v := range subscribeUsers {
if senderId != v.Name && !memberMap[v.Name] {
memberMap[v.Name] = true
}
}
var wg sync.WaitGroup
if senderId != receiverId {
notification := Notification{
// Id: memberMap[receiverId],
NotificationType: 1,
ObjectId: objectId,
CreatedTime: util.GetCurrentTime(),
SenderId: senderId,
ReceiverId: receiverId,
Status: 1,
}
_ = AddNotification(&notification)
// send remind email
reminder, email := GetMemberEmailReminder(receiverId)
if email != "" && reminder {
topicIdStr := util.IntToString(topicId)
err := sendRemindMail(topicInfo.Title, content, topicIdStr, senderId, email, Domain)
if err != nil {
panic(err)
}
}
}
delete(memberMap, receiverId)
for receiverId2 := range memberMap {
wg.Add(1)
go func(receiverId2 string) {
defer wg.Done()
notification := Notification{
NotificationType: 2,
ObjectId: objectId,
CreatedTime: util.GetCurrentTime(),
SenderId: senderId,
ReceiverId: receiverId2,
Status: 1,
}
_ = AddNotification(&notification)
// send remind email
reminder, email := GetMemberEmailReminder(receiverId2)
if email != "" && reminder {
topicIdStr := util.IntToString(topicId)
err := sendRemindMail(topicInfo.Title, content, topicIdStr, senderId, email, Domain)
if err != nil {
panic(err)
}
}
}(receiverId2)
}
wg.Wait()
}
func AddTopicNotification(objectId int, author, content string) {
var wg sync.WaitGroup
memberMap := make(map[string]bool)
reg := regexp.MustCompile("@(.*?)[ \n\t]")
reg2 := regexp.MustCompile("@([^ \n\t]*?)[^ \n\t]$")
regResult := reg.FindAllStringSubmatch(content, -1)
regResult2 := reg2.FindAllStringSubmatch(content, -1)
for _, v := range regResult {
if author != v[1] && !memberMap[v[1]] {
memberMap[v[1]] = true
AddMemberFavorites(v[1], "subscribe_topic", strconv.Itoa(objectId))
}
}
for _, v := range regResult2 {
v[1] += content[len(content)-1:]
if author != v[1] && !memberMap[v[1]] {
memberMap[v[1]] = true
AddMemberFavorites(v[1], "subscribe_topic", strconv.Itoa(objectId))
}
}
for k := range memberMap {
wg.Add(1)
k := k
go func() {
defer wg.Done()
notification := Notification{
NotificationType: 3,
ObjectId: objectId,
CreatedTime: util.GetCurrentTime(),
SenderId: author,
ReceiverId: k,
Status: 1,
}
_ = AddNotification(&notification)
// send remind email
reminder, email := GetMemberEmailReminder(k)
if email != "" && reminder {
topicIdStr := util.IntToString(objectId)
err := sendRemindMail(GetTopicTitle(objectId), content, topicIdStr, author, email, Domain)
if err != nil {
panic(err)
}
}
}()
}
wg.Wait()
}
func sendRemindMail(title string, content string, topicId string, sender string, receiver string, domain string) error {
fromName := ""
conf := GetFrontConfById("forumName")
if conf != nil {
fromName = conf.Value
}
if fromName == "" {
fromName = beego.AppConfig.String("appname")
}
return service.SendRemindMail(fromName, title, content, topicId, sender, receiver, domain)
}

159
object/plane.go Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "sync"
type Plane struct {
Id string `xorm:"varchar(50) notnull pk" json:"id"`
Name string `xorm:"varchar(50)" json:"name"`
Sorter int `xorm:"int" json:"-"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Image string `xorm:"varchar(200)" json:"image"`
BackgroundColor string `xorm:"varchar(20)" json:"backgroundColor"`
Color string `xorm:"varchar(20)" json:"color"`
Visible bool `xorm:"bool" json:"-"`
}
func GetPlanes() []*Plane {
planes := []*Plane{}
err := adapter.Engine.Asc("sorter").Where("visible = ?", 1).Find(&planes)
if err != nil {
panic(err)
}
return planes
}
func GetAllPlanes() []*AdminPlaneInfo {
planes := []*Plane{}
err := adapter.Engine.Asc("sorter").Find(&planes)
if err != nil {
panic(err)
}
res := []*AdminPlaneInfo{}
for _, v := range planes {
temp := AdminPlaneInfo{
Plane: *v,
Sorter: v.Sorter,
Visible: v.Visible,
NodesNum: GetPlaneNodesNum(v.Id),
}
res = append(res, &temp)
}
return res
}
func GetPlane(id string) *Plane {
plane := Plane{Id: id}
existed, err := adapter.Engine.Get(&plane)
if err != nil {
panic(err)
}
if existed {
return &plane
} else {
return nil
}
}
func GetPlaneAdmin(id string) *AdminPlaneInfo {
plane := Plane{Id: id}
existed, err := adapter.Engine.Get(&plane)
if err != nil {
panic(err)
}
planeNode := GetNodeFromPlane(plane.Id)
res := AdminPlaneInfo{
Plane: plane,
Sorter: plane.Sorter,
Visible: plane.Visible,
NodesNum: len(planeNode),
Nodes: planeNode,
}
if existed {
return &res
} else {
return nil
}
}
func AddPlane(plane *Plane) bool {
affected, err := adapter.Engine.Insert(plane)
if err != nil {
panic(err)
}
return affected != 0
}
func UpdatePlane(id string, plane *Plane) bool {
if GetPlane(id) == nil {
return false
}
affected, err := adapter.Engine.Id(id).AllCols().Update(plane)
if err != nil {
panic(err)
}
return affected != 0
}
func GetPlaneList() []*PlaneWithNodes {
planes := GetPlanes()
var wg sync.WaitGroup
res := make([]*PlaneWithNodes, len(planes))
for k, plane := range planes {
plane := plane
k := k
wg.Add(1)
go func() {
defer wg.Done()
temp := &PlaneWithNodes{
Plane: plane,
Nodes: GetNodeFromPlane(plane.Id),
}
res[k] = temp
}()
}
wg.Wait()
return res
}
func DeletePlane(id string) bool {
affected, err := adapter.Engine.Id(id).Delete(&Plane{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetPlaneNodesNum(id string) int {
node := new(Node)
total, err := adapter.Engine.Where("plane_id = ?", id).Count(node)
if err != nil {
panic(err)
}
return int(total)
}

59
object/poster.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
type Poster struct {
Id string `xorm:"varchar(50) notnull pk" json:"id"`
Advertiser string `xorm:"varchar(40)" json:"advertiser"`
Link string `xorm:"varchar(500)" json:"link"`
PictureLink string `xorm:"varchar(500)" json:"picture_link"`
State string `xorm:"varchar(10)" json:"state"`
}
func AddPoster(poster Poster) bool {
affected, err := adapter.Engine.Insert(poster)
if err != nil {
panic(err)
}
return affected != 0
}
func GetPoster(id string) *Poster {
poster := Poster{Id: id}
existed, err := adapter.Engine.Get(&poster)
if err != nil {
panic(err)
}
if existed {
return &poster
} else {
return nil
}
}
func UpdatePoster(id string, poster Poster) bool {
if GetPoster(id) == nil {
return AddPoster(poster)
}
affected, err := adapter.Engine.Id(id).AllCols().Update(poster)
if err != nil {
panic(err)
}
return affected != 0
}

70
object/proxy.go Normal file
View File

@ -0,0 +1,70 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"net"
"net/http"
"time"
"github.com/astaxie/beego"
"golang.org/x/net/proxy"
)
var httpClient *http.Client
func InitHttpClient() {
httpClient = getProxyHttpClient()
}
func isAddressOpen(address string) bool {
timeout := time.Millisecond * 100
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
// cannot connect to address, proxy is not active
return false
}
if conn != nil {
defer conn.Close()
fmt.Printf("Socks5 proxy enabled: %s\n", address)
return true
}
return false
}
func getProxyHttpClient() *http.Client {
httpProxy := beego.AppConfig.String("httpProxy")
if httpProxy == "" {
return &http.Client{}
}
if !isAddressOpen(httpProxy) {
return &http.Client{}
}
// https://stackoverflow.com/questions/33585587/creating-a-go-socks5-client
dialer, err := proxy.SOCKS5("tcp", httpProxy, nil, proxy.Direct)
if err != nil {
panic(err)
}
tr := &http.Transport{Dial: dialer.Dial}
return &http.Client{
Transport: tr,
}
}

510
object/reply.go Normal file
View File

@ -0,0 +1,510 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"time"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
type Reply struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
Author string `xorm:"varchar(100) index" json:"author"`
TopicId int `xorm:"int index" json:"topicId"`
ParentId int `xorm:"int" json:"parentId"`
Tags []string `xorm:"varchar(200)" json:"tags"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Deleted bool `xorm:"bool" json:"deleted"`
IsHidden bool `xorm:"bool" json:"isHidden"`
ThanksNum int `xorm:"int" json:"thanksNum"`
EditorType string `xorm:"varchar(40)" json:"editorType"`
Content string `xorm:"mediumtext" json:"content"`
Ip string `xorm:"varchar(100)" json:"ip"`
State string `xorm:"varchar(100)" json:"state"`
GitterMessageId string `xorm:"varchar(100)" json:"gitterMessageId"`
}
var enableNestedReply, _ = beego.AppConfig.Bool("enableNestedReply")
// GetReplyCount returns all replies num so far, both deleted and not deleted.
func GetReplyCount() int {
count, err := adapter.Engine.Count(&Reply{})
if err != nil {
panic(err)
}
return int(count)
}
// @Title GetReplies
// @router /get-replies [get]
// @Description GetReplies returns more information about reply of a topic.
// @Tag Reply API
func GetReplies(topicId int, user *casdoorsdk.User, limit int, page int) ([]*ReplyWithAvatar, int) {
replies := []*ReplyWithAvatar{}
realPage := page
err := adapter.Engine.Table("reply").
Join("LEFT OUTER", "consumption_record", "consumption_record.object_id = reply.id and consumption_record.consumption_type = ?", 5).
Where("reply.topic_id = ?", topicId).
Asc("reply.created_time").
Cols("reply.*, consumption_record.amount").
Find(&replies)
if err != nil {
panic(err)
}
for _, reply := range replies {
reply.Avatar = getUserAvatar(reply.Author)
}
isAdmin := CheckIsAdmin(user)
for _, v := range replies {
v.ThanksStatus = v.ConsumptionAmount != 0
v.Deletable = isAdmin || ReplyDeletable(v.CreatedTime, GetUserName(user), v.Author)
v.Editable = isAdmin || GetReplyEditableStatus(GetUserName(user), v.Author, v.CreatedTime)
}
var resultReplies []*ReplyWithAvatar
if enableNestedReply {
replies = bulidReplies(replies)
// Use limit to calculate offset
// If limit is 2, but the first reply have 2 child replies(3 replies)
// We need put these replies to offset, so cannot use (page * limit) to calculate offset
pageLimit := limit
for index, reply := range replies {
replyLen := getReplyLen(reply)
// Ignore replies until page == 1
if page > 1 {
// Calculate limit in every ignore page
pageLimit -= replyLen
// Get replices for init == true(get the latest replies)
resultReplies = append(resultReplies, reply)
if pageLimit <= 0 {
page--
pageLimit = limit
if index+1 < len(replies) {
// If the page is a usable value when we get the latest replies, clear the result
resultReplies = nil
}
}
} else if limit > 0 {
// if page == 1, prove that we are processing current page now
// So we can only calculate the limit and put replies to result slice
limit -= replyLen
resultReplies = append(resultReplies, reply)
page--
} else {
// if page == 1, and limit < 0, prove that we get all replies in this page now
break
}
}
if page > 0 {
realPage -= page
}
} else {
offset := page*limit - limit
for _, reply := range replies {
if offset > 0 {
offset--
} else {
if limit > 0 {
resultReplies = append(resultReplies, reply)
} else {
break
}
}
}
}
return resultReplies, realPage
}
func makeReplyTree(replies []*ReplyWithAvatar, reply *ReplyWithAvatar) bool {
if len(replies) == 0 {
return false
}
for _, r := range replies {
if r.Id == reply.ParentId {
r.Child = append(r.Child, reply)
return true
} else {
if makeReplyTree(r.Child, reply) {
return true
}
}
}
return false
}
func getReplyLen(reply *ReplyWithAvatar) int {
replyLen := 1
for _, child := range reply.Child {
replyLen += getReplyLen(child)
}
return replyLen
}
func bulidReplies(replies []*ReplyWithAvatar) []*ReplyWithAvatar {
var childReplies []*ReplyWithAvatar
var repliesResult []*ReplyWithAvatar
for _, reply := range replies {
if reply.ParentId != 0 {
childReplies = append(childReplies, reply)
} else {
repliesResult = append(repliesResult, reply)
}
if reply.Deleted {
reply.Content = ""
}
}
replies = repliesResult
for _, child := range childReplies {
makeReplyTree(replies, child)
}
return replies
}
func GetRepliesOfTopic(topicId int) []Reply {
var ret []Reply
err := adapter.Engine.Where("topic_id = ?", topicId).And("deleted = ?", 0).Find(&ret)
if err != nil {
panic(err)
}
return ret
}
// GetTopicReplyNum returns topic's reply num.
func GetTopicReplyNum(topicId int) int {
var total int64
var err error
reply := new(Reply)
total, err = adapter.Engine.Where("topic_id = ?", topicId).And("deleted = ?", 0).Count(reply)
if err != nil {
panic(err)
}
return int(total)
}
// GetReplyByContentAndAuthor returns reply by content and author.
func GetReplyByContentAndAuthor(content string, author string) []*Reply {
var ret []*Reply
err := adapter.Engine.Where("content = ?", content).And("author = ?", author).Find(&ret)
if err != nil {
panic(err)
}
return ret
}
// GetLatestReplyInfo returns topic's latest reply information.
func GetLatestReplyInfo(topicId int) *Reply {
var reply Reply
exist, err := adapter.Engine.Where("topic_id = ?", topicId).And("deleted = ?", false).Desc("created_time").Limit(1).Omit("content").Get(&reply)
if err != nil {
panic(err)
}
if exist {
return &reply
}
return nil
}
// GetReply returns a single reply.
func GetReply(id int) *Reply {
reply := Reply{Id: id}
existed, err := adapter.Engine.Get(&reply)
if err != nil {
panic(err)
}
if existed {
return &reply
}
return nil
}
// GetReplyWithDetails returns more information about reply, including avatar, thanks status, deletable and editable.
func GetReplyWithDetails(user *casdoorsdk.User, id int) *ReplyWithAvatar {
reply := ReplyWithAvatar{}
existed, err := adapter.Engine.Table("reply").
Join("LEFT OUTER", "consumption_record", "consumption_record.object_id = reply.id and consumption_record.consumption_type = ?", 5).
Id(id).Cols("reply.*, consumption_record.amount").Get(&reply)
if err != nil {
panic(err)
}
reply.Avatar = getUserAvatar(reply.Author)
isAdmin := CheckIsAdmin(user)
if existed {
reply.ThanksStatus = reply.ConsumptionAmount != 0
reply.Deletable = isAdmin || ReplyDeletable(reply.CreatedTime, GetUserName(user), reply.Author)
reply.Editable = isAdmin || GetReplyEditableStatus(GetUserName(user), reply.Author, reply.CreatedTime)
return &reply
}
return nil
}
/*
func GetReplyId() int {
reply := new(Reply)
_, err := adapter.Engine.Desc("created_time").Omit("content").Limit(1).Get(reply)
if err != nil {
panic(err)
}
res := util.ParseInt(reply.Id) + 1
return res
}
*/
// UpdateReply updates reply's all field.
func UpdateReply(id int, reply *Reply) bool {
if GetReply(id) == nil {
return false
}
reply.Content = FilterUnsafeHTML(reply.Content)
_, err := adapter.Engine.Id(id).AllCols().Update(reply)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
// UpdateReplyWithLimitCols updates reply's not null field.
func UpdateReplyWithLimitCols(id int, reply *Reply) bool {
if GetReply(id) == nil {
return false
}
reply.Content = FilterUnsafeHTML(reply.Content)
_, err := adapter.Engine.Id(id).Update(reply)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
// AddReply returns add reply result and reply id.
func AddReply(reply *Reply) (bool, int) {
// reply.Content = strings.ReplaceAll(reply.Content, "\n", "<br/>")
reply.Content = FilterUnsafeHTML(reply.Content)
affected, err := adapter.Engine.Insert(reply)
if err != nil {
panic(err)
}
return affected != 0, reply.Id
}
func AddReplies(replies []*Reply) bool {
affected, err := adapter.Engine.Insert(replies)
if err != nil {
panic(err)
}
return affected != 0
}
func AddRepliesInBatch(relies []*Reply) bool {
batchSize := 1000
if len(relies) == 0 {
return false
}
affected := false
for i := 0; i < (len(relies)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
if end > len(relies) {
end = len(relies)
}
tmp := relies[start:end]
fmt.Printf("Add relies: [%d - %d].\n", start, end)
if AddReplies(tmp) {
affected = true
}
}
return affected
}
/*
func DeleteReply(id string) bool {
affected, err := adapter.Engine.Id(id).Delete(&Reply{})
if err != nil {
panic(err)
}
return affected != 0
}
*/
func DeleteRepliesHardByTopicId(topicId int) bool {
affected, err := adapter.Engine.Where("topic_id = ?", topicId).Delete(&Reply{})
if err != nil {
panic(err)
}
return affected != 0
}
// DeleteReply soft delete reply.
func DeleteReply(id int) bool {
reply := new(Reply)
reply.Deleted = true
affected, err := adapter.Engine.Id(id).Cols("deleted").Update(reply)
if err != nil {
panic(err)
}
return affected != 0
}
// GetLatestReplies returns member's latest replies.
func GetLatestReplies(author string, limit int, offset int) []*LatestReply {
replys := []*LatestReply{}
err := adapter.Engine.Table("reply").Join("LEFT OUTER", "topic", "topic.id = reply.topic_id").
Where("reply.author = ?", author).And("reply.deleted = ?", 0).
Desc("reply.created_time").
Cols("reply.content, reply.author, reply.created_time, topic.id, topic.node_id, topic.node_name, topic.title, topic.author").
Limit(limit, offset).Find(&replys)
if err != nil {
panic(err)
}
return replys
}
// GetMemberRepliesNum returns member's all replies num.
func GetMemberRepliesNum(memberId string) int {
var total int64
var err error
reply := new(Reply)
total, err = adapter.Engine.Where("author = ?", memberId).And("deleted = ?", 0).Count(reply)
if err != nil {
panic(err)
}
return int(total)
}
// GetReplyTopicTitle only returns reply's topic title.
func GetReplyTopicTitle(id int) string {
topic := Topic{Id: id}
existed, err := adapter.Engine.Cols("title").Get(&topic)
if err != nil {
panic(err)
}
if existed {
return topic.Title
}
return ""
}
// GetReplyAuthor only returns reply's topic author.
func GetReplyAuthor(id int) *casdoorsdk.User {
reply := Reply{Id: id}
existed, err := adapter.Engine.Cols("author").Get(&reply)
if err != nil {
panic(err)
}
if !existed {
return nil
}
return GetUser(reply.Author)
}
// AddReplyThanksNum updates reply's thanks num.
func AddReplyThanksNum(id int) bool {
affected, err := adapter.Engine.ID(id).Incr("thanks_num", 1).Update(Reply{})
if err != nil {
panic(err)
}
return affected != 0
}
// ReplyDeletable checks whether the reply can be deleted.
func ReplyDeletable(date, memberId, author string) bool {
if memberId != author {
return false
}
t, err := time.Parse("2006-01-02T15:04:05+08:00", date)
if err != nil {
return false
}
h, _ := time.ParseDuration("-1h")
t = t.Add(8 * h)
now := time.Now()
if now.Sub(t).Minutes() > ReplyDeletableTime {
return false
}
return true
}
// GetReplyEditableStatus checks whether the reply can be edited.
func GetReplyEditableStatus(member, author, createdTime string) bool {
if member != author {
return false
}
t, err := time.Parse("2006-01-02T15:04:05+08:00", createdTime)
if err != nil {
return false
}
h, _ := time.ParseDuration("-1h")
t = t.Add(8 * h)
now := time.Now()
if now.Sub(t).Minutes() > ReplyEditableTime {
return false
}
return true
}
func SearchReplies(keyword string) []Reply {
var ret []Reply
keyword = fmt.Sprintf("%%%s%%", keyword)
err := adapter.Engine.Where("deleted = 0").Where("content like ?", keyword).Find(&ret)
if err != nil {
panic(err)
}
return ret
}

89
object/sensitive_word.go Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "strings"
type SensitiveWord struct {
Word string `xorm:"varchar(64) notnull"`
Id int64
}
var sensitiveWords []SensitiveWord
func loadSensitiveWords() {
if len(sensitiveWords) == 0 {
err := adapter.Engine.Desc("word").Find(&sensitiveWords)
if err != nil {
panic(err)
}
}
}
func AddSensitiveWord(word string) {
if IsSensitiveWord(word) {
return
}
_, err := adapter.Engine.Insert(SensitiveWord{Word: word})
if err != nil {
panic(err)
}
sensitiveWords = nil
err = adapter.Engine.Desc("word").Find(&sensitiveWords)
if err != nil {
panic(err)
}
}
func DeleteSensitiveWord(word string) {
_, err := adapter.Engine.Delete(SensitiveWord{Word: word})
if err != nil {
panic(err)
}
sensitiveWords = nil
err = adapter.Engine.Desc("word").Find(&sensitiveWords)
if err != nil {
panic(err)
}
}
func IsSensitiveWord(word string) bool {
loadSensitiveWords()
for _, wordObj := range sensitiveWords {
if word == wordObj.Word {
return true
}
}
return false
}
func GetSensitiveWords() []string {
loadSensitiveWords()
var ret []string
for _, wordObj := range sensitiveWords {
ret = append(ret, wordObj.Word)
}
return ret
}
func ContainsSensitiveWord(str string) bool {
loadSensitiveWords()
for _, wordObj := range sensitiveWords {
if strings.Index(str, wordObj.Word) >= 0 {
return true
}
}
return false
}

193
object/tab.go Normal file
View File

@ -0,0 +1,193 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
type Tab struct {
Id string `xorm:"varchar(100) notnull pk" json:"id"`
Name string `xorm:"varchar(100)" json:"name"`
Sorter int `json:"sorter"`
Ranking int `json:"ranking"`
CreatedTime string `xorm:"varchar(40)" json:"-"`
DefaultNode string `xorm:"varchar(100)" json:"defaultNode"`
HomePage bool `xorm:"bool" json:"-"`
Desc string `xorm:"mediumtext" json:"desc"`
Extra string `xorm:"mediumtext" json:"extra"`
Moderators []string `xorm:"varchar(200)" json:"moderators"`
}
func GetTab(id string) *Tab {
tab := Tab{Id: id}
existed, err := adapter.Engine.Get(&tab)
if err != nil {
panic(err)
}
if existed {
return &tab
} else {
return nil
}
}
func AddTab(tab *Tab) bool {
affected, err := adapter.Engine.Insert(tab)
if err != nil {
panic(err)
}
return affected != 0
}
func AddTabs(tabs []*Tab) bool {
affected, err := adapter.Engine.Insert(tabs)
if err != nil {
panic(err)
}
return affected != 0
}
func UpdateTab(id string, tab *Tab) bool {
if GetTab(id) == nil {
return false
}
_, err := adapter.Engine.Id(id).AllCols().Omit("id").Update(tab)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
func DeleteTab(id string) bool {
affected, err := adapter.Engine.Id(id).Delete(&Tab{})
if err != nil {
panic(err)
}
return affected != 0
}
func GetHomePageTabs() []*Tab {
tabs := []*Tab{}
err := adapter.Engine.Asc("sorter").Where("home_page = ?", 1).Find(&tabs)
if err != nil {
panic(err)
}
return tabs
}
func GetAllTabs() []*Tab {
tabs := []*Tab{}
err := adapter.Engine.Asc("sorter").Find(&tabs)
if err != nil {
panic(err)
}
return tabs
}
// GetTabAdmin returns more tab information for admin.
func GetTabAdmin(id string) *AdminTabInfo {
tab := Tab{Id: id}
existed, err := adapter.Engine.Get(&tab)
if err != nil {
panic(err)
}
if existed {
var topicsNum int
nodes := GetNodeFromTab(tab.Id)
for _, v := range nodes {
topicsNum += GetNodeTopicNum(v.Id)
}
res := AdminTabInfo{
Id: tab.Id,
Name: tab.Name,
Sorter: tab.Sorter,
CreatedTime: tab.CreatedTime,
DefaultNode: tab.DefaultNode,
HomePage: tab.HomePage,
NodesNum: len(nodes),
TopicsNum: topicsNum,
}
return &res
} else {
return nil
}
}
func GetAllTabsAdmin() []*AdminTabInfo {
tabs := []*Tab{}
err := adapter.Engine.Asc("sorter").Find(&tabs)
if err != nil {
panic(err)
}
res := []*AdminTabInfo{}
for _, v := range tabs {
var topicsNum int
nodes := GetNodeFromTab(v.Id)
for _, v := range nodes {
topicsNum += GetNodeTopicNum(v.Id)
}
temp := AdminTabInfo{
Id: v.Id,
Name: v.Name,
Sorter: v.Sorter,
CreatedTime: v.CreatedTime,
DefaultNode: v.DefaultNode,
HomePage: v.HomePage,
NodesNum: len(nodes),
TopicsNum: topicsNum,
}
res = append(res, &temp)
}
return res
}
func GetDefaultTab() string {
var tab Tab
_, err := adapter.Engine.Where("home_page = ?", 1).Asc("sorter").Limit(1).Get(&tab)
if err != nil {
panic(err)
}
return tab.Id
}
func GetNodesByTab(id string) []*Node {
nodes := []*Node{}
num := HomePageNodeNum
if id == "all" {
err := adapter.Engine.Cols("id, name").Desc("sorter").Limit(num).Find(&nodes)
if err != nil {
panic(err)
}
} else {
err := adapter.Engine.Where("tab_id = ?", id).Cols("id, name").Desc("sorter").Limit(num).Find(&nodes)
if err != nil {
panic(err)
}
}
return nodes
}

42
object/tab_test.go Normal file
View File

@ -0,0 +1,42 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"testing"
)
func TestSyncTabsForTopics(t *testing.T) {
InitConfig()
InitAdapter()
nodes := GetNodes()
nodeTabMap := map[string]string{}
for _, node := range nodes {
nodeTabMap[node.Id] = node.TabId
}
topics := GetAllTopics()
for i, topic := range topics {
tabId := nodeTabMap[topic.NodeId]
topic.TabId = tabId
affected := updateTopicSimple(topic.Id, topic)
if affected == false {
panic(fmt.Errorf("TestSyncTabsForTopics() error, affected == false"))
}
fmt.Printf("[%d/%d]: Synced tab for topic: [%d, %s] as tab: %s\n", i+1, len(topics), topic.Id, topic.Author, topic.TabId)
}
}

806
object/topic.go Normal file
View File

@ -0,0 +1,806 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strings"
"sync"
"time"
"github.com/casbin/casnode/util"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
"github.com/gomarkdown/markdown"
)
type Topic struct {
Id int `xorm:"int notnull pk autoincr" json:"id"`
Author string `xorm:"varchar(100) index" json:"author"`
NodeId string `xorm:"varchar(100) index" json:"nodeId"`
NodeName string `xorm:"varchar(100)" json:"nodeName"`
TabId string `xorm:"varchar(100) index" json:"tabId"`
Title string `xorm:"varchar(300) index" json:"title"`
CreatedTime string `xorm:"varchar(40)" json:"createdTime"`
Tags []string `xorm:"varchar(200)" json:"tags"`
ReplyCount int `json:"replyCount"`
UpCount int `json:"upCount"`
DownCount int `json:"downCount"`
HitCount int `json:"hitCount"`
Hot int `xorm:"index" json:"hot"`
FavoriteCount int `json:"favoriteCount"`
SubscribeCount int `json:"subscribeCount"`
HomePageTopTime string `xorm:"varchar(40) index(IDX_topic_htt_lrt)" json:"homePageTopTime"`
TabTopTime string `xorm:"varchar(40) index(IDX_topic_ttt_lrt)" json:"tabTopTime"`
NodeTopTime string `xorm:"varchar(40) index(IDX_topic_ntt_lrt)" json:"nodeTopTime"`
LastReplyUser string `xorm:"varchar(100)" json:"lastReplyUser"`
LastReplyTime string `xorm:"varchar(40) index(IDX_topic_htt_lrt) index(IDX_topic_ttt_lrt) index(IDX_topic_ntt_lrt)" json:"lastReplyTime"`
Deleted bool `xorm:"bool index" json:"-"`
EditorType string `xorm:"varchar(40)" json:"editorType"`
Content string `xorm:"mediumtext" json:"content"`
UrlPath string `xorm:"varchar(100)" json:"urlPath"`
IsHidden bool `xorm:"bool index" json:"isHidden"`
Ip string `xorm:"varchar(100)" json:"ip"`
State string `xorm:"varchar(100)" json:"state"`
GitterMessageId string `xorm:"varchar(100)" json:"gitterMessageId"`
}
func GetTopicCount() int {
count, err := adapter.Engine.Count(&Topic{})
if err != nil {
panic(err)
}
return int(count)
}
func GetTopicNum() int {
count, err := adapter.Engine.Where("deleted = ? and is_hidden = ?", 0, 0).Count(&Topic{})
if err != nil {
panic(err)
}
return int(count)
}
func GetCreatedTopicsNum(memberId string) int {
topic := new(Topic)
total, err := adapter.Engine.Where("author = ? and deleted = ? and is_hidden = ?", memberId, 0, 0).Count(topic)
if err != nil {
panic(err)
}
return int(total)
}
func getAvataredTopics(topics []*Topic) []*TopicWithAvatar {
res := []*TopicWithAvatar{}
for _, topic := range topics {
topicWithAvatar := &TopicWithAvatar{
Topic: *topic,
Avatar: getUserAvatar(topic.Author),
}
res = append(res, topicWithAvatar)
}
return res
}
func GetTopics(limit int, offset int) []*TopicWithAvatar {
var topics []*Topic
err := adapter.Engine.Table("topic").
Where("deleted = ?", 0).And("is_hidden = ?", 0).
Desc("home_page_top_time", "last_reply_time").
Cols("id, author, node_id, node_name, title, created_time, last_reply_user, last_Reply_time, reply_count, favorite_count, deleted, home_page_top_time, tab_top_time, node_top_time").
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
return getAvataredTopics(topics)
}
func GetAllTopics() []*Topic {
var topics []*Topic
err := adapter.Engine.Find(&topics)
if err != nil {
panic(err)
}
return topics
}
func GetTopicsByTitleAndAuthor(title string, author string) []*Topic {
topics := []*Topic{}
err := adapter.Engine.Where("title = ?", title).And("author = ?", author).Find(&topics)
if err != nil {
panic(err)
}
return topics
}
// GetTopicsAdmin *sort: 1 means Asc, 2 means Desc, 0 means no effect.
func GetTopicsAdmin(usernameSearchKw, titleSearchKw, contentSearchKw, showDeletedTopic, createdTimeSort, lastReplySort, usernameSort, replyCountSort, hotSort, favCountSort string, limit int, offset int) ([]*AdminTopicInfo, int) {
topics := []*Topic{}
db := adapter.Engine.Table("topic")
// created time sort
switch createdTimeSort {
case "1":
db = db.Asc("created_time")
case "2":
db = db.Desc("created_time")
}
// last reply time sort
switch lastReplySort {
case "1":
db = db.Asc("last_reply_time")
case "2":
db = db.Desc("last_reply_time")
}
// author sort
switch usernameSort {
case "1":
db = db.Asc("author")
case "2":
db = db.Desc("author")
}
// reply count sort
switch replyCountSort {
case "1":
db = db.Asc("reply_count")
case "2":
db = db.Desc("reply_count")
}
// hot sort
switch hotSort {
case "1":
db = db.Asc("hot")
case "2":
db = db.Desc("hot")
}
// favorite count sort
switch favCountSort {
case "1":
db = db.Asc("favorite_count")
case "2":
db = db.Desc("favorite_count")
}
if usernameSearchKw != "" {
unKw := util.SplitWords(usernameSearchKw)
for _, v := range unKw {
db.Or("author like ?", "%"+v+"%")
}
}
if titleSearchKw != "" {
tiKw := util.SplitWords(titleSearchKw)
for _, v := range tiKw {
db.Or("title like ?", "%"+v+"%")
}
}
if contentSearchKw != "" {
coKw := util.SplitWords(contentSearchKw)
for _, v := range coKw {
db.Or("content like ?", "%"+v+"%")
}
}
if showDeletedTopic == "0" {
db = db.Where("deleted = ?", 0)
}
num, err := db.Limit(limit, offset).FindAndCount(&topics, &Topic{})
if err != nil {
panic(err)
}
res := []*AdminTopicInfo{}
for _, v := range topics {
temp := AdminTopicInfo{
Topic: *v,
Deleted: v.Deleted,
}
res = append(res, &temp)
}
return res, int(num)
}
func GetTopicWithAvatar(id int, user *casdoorsdk.User) *TopicWithAvatar {
topic := TopicWithAvatar{}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
topicObj := GetTopic(id)
if topicObj != nil {
topic.Topic = *topicObj
topic.Avatar = getUserAvatar(topic.Author)
topic.Editable = GetTopicEditableStatus(user, topic.Author, topic.NodeId, topic.CreatedTime)
}
}()
go func() {
defer wg.Done()
name := ""
if user != nil {
name = GetUserName(user)
}
topic.ThanksStatus = GetThanksStatus(name, id, 4)
}()
wg.Wait()
if topic.Author == "" {
return nil
}
return &topic
}
func GetTopic(id int) *Topic {
topic := Topic{Id: id}
existed, err := adapter.Engine.Get(&topic)
if err != nil {
panic(err)
}
if existed {
return &topic
} else {
return nil
}
}
func GetTopicByUrlPathAndTitle(urlPath, title, nodeId string) *Topic {
topic := Topic{UrlPath: urlPath, Title: title, NodeId: nodeId}
existed, err := adapter.Engine.Get(&topic)
if err != nil {
panic(err)
}
if !existed {
return nil
}
return &topic
}
func GetTopicBasicInfo(id int) *Topic {
topic := Topic{Id: id}
existed, err := adapter.Engine.Id(id).Omit("content").Get(&topic)
if err != nil {
panic(err)
}
if existed {
return &topic
} else {
return nil
}
}
func GetTopicAdmin(id int) *AdminTopicInfo {
topic := Topic{Id: id}
existed, err := adapter.Engine.Get(&topic)
if err != nil {
panic(err)
}
if existed {
return &AdminTopicInfo{
Topic: topic,
Deleted: topic.Deleted,
}
} else {
return nil
}
}
func GetTopicTitle(id int) string {
topic := Topic{Id: id}
existed, err := adapter.Engine.Cols("title").Get(&topic)
if err != nil {
panic(err)
}
if existed {
return topic.Title
} else {
return ""
}
}
func GetTopicAuthor(id int) *casdoorsdk.User {
topic := Topic{Id: id}
existed, err := adapter.Engine.Cols("author").Get(&topic)
if err != nil {
panic(err)
}
if !existed {
return nil
}
return GetUser(topic.Author)
}
func GetTopicNodeId(id int) string {
topic := Topic{Id: id}
existed, err := adapter.Engine.Cols("node_id").Get(&topic)
if err != nil {
panic(err)
}
if existed {
return topic.NodeId
} else {
return ""
}
}
func GetTopicsByNode(nodeId string, limit int, offset int) []*NodeTopic {
topics := []*NodeTopic{}
err := adapter.Engine.Table("topic").
Where("node_id = ?", nodeId).And("deleted = ?", 0).
Desc("node_top_time", "last_reply_time").
Cols("id, author, node_id, node_name, title, created_time, last_reply_user, last_Reply_time, reply_count, favorite_count, deleted, home_page_top_time, tab_top_time, node_top_time").
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
for _, topic := range topics {
topic.Avatar = getUserAvatar(topic.Author)
topic.ContentLength = len(topic.Content)
topic.Content = ""
}
return topics
}
func GetTopicsByTag(tagId string, limit int, offset int) []*NodeTopic {
topics := []*NodeTopic{}
tag := fmt.Sprintf("%%%q%%", tagId)
err := adapter.Engine.Table("topic").
Where("deleted = ?", 0).And("tags LIKE ?", tag).
Desc("node_top_time", "last_reply_time").
Cols("id, author, node_id, node_name, title, created_time, last_reply_user, last_Reply_time, reply_count, favorite_count, deleted, home_page_top_time, tab_top_time, node_top_time").
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
for _, topic := range topics {
topic.Avatar = getUserAvatar(topic.Author)
topic.ContentLength = len(topic.Content)
topic.Content = ""
}
return topics
}
func UpdateTopic(id int, topic *Topic) bool {
if GetTopic(id) == nil {
return false
}
topic.Content = FilterUnsafeHTML(topic.Content)
_, err := adapter.Engine.Id(id).AllCols().Update(topic)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
func updateTopicSimple(id int, topic *Topic) bool {
affected, err := adapter.Engine.Id(id).AllCols().Update(topic)
if err != nil {
panic(err)
}
return affected != 0
}
func UpdateTopicWithLimitCols(id int, topic *Topic) bool {
if GetTopic(id) == nil {
return false
}
topic.Content = FilterUnsafeHTML(topic.Content)
_, err := adapter.Engine.Id(id).Update(topic)
if err != nil {
panic(err)
}
// return affected != 0
return true
}
// AddTopic return add topic result and topic id
func AddTopic(topic *Topic) (bool, int) {
topic.Content = FilterUnsafeHTML(topic.Content)
affected, err := adapter.Engine.Insert(topic)
if err != nil {
panic(err)
}
return affected != 0, topic.Id
}
func AddTopics(topics []*Topic) bool {
affected, err := adapter.Engine.Insert(topics)
if err != nil {
panic(err)
}
return affected != 0
}
func AddTopicsInBatch(topics []*Topic) bool {
batchSize := 1000
if len(topics) == 0 {
return false
}
affected := false
for i := 0; i < (len(topics)-1)/batchSize+1; i++ {
start := i * batchSize
end := (i + 1) * batchSize
if end > len(topics) {
end = len(topics)
}
tmp := topics[start:end]
fmt.Printf("Add topics: [%d - %d].\n", start, end)
if AddTopics(tmp) {
affected = true
}
}
return affected
}
func DeleteTopicHard(id int) bool {
affected, err := adapter.Engine.Id(id).Delete(&Topic{})
if err != nil {
panic(err)
}
return affected != 0
}
func DeleteTopic(id int) bool {
t := GetTopic(id)
if strings.HasPrefix(t.Content, "URL: ") {
return DeleteTopicHard(id)
}
topic := new(Topic)
topic.Deleted = true
affected, err := adapter.Engine.Id(id).Cols("deleted").Update(topic)
if err != nil {
panic(err)
}
return affected != 0
}
/*
func GetTopicId() int {
topic := new(Topic)
_, err := adapter.Engine.Desc("created_time").Omit("content").Limit(1).Get(topic)
if err != nil {
panic(err)
}
res := util.ParseInt(topic.Id) + 1
return res
}
*/
func GetAllCreatedTopics(author string, tab string, limit int, offset int) []*Topic {
topics := []*Topic{}
err := adapter.Engine.Desc("created_time").Where("author = ?", author).And("deleted = ?", 0).Omit("content").Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
return topics
}
func AddTopicHitCount(topicId int) bool {
affected, err := adapter.Engine.ID(topicId).Incr("hit_count", 1).Update(Topic{})
if err != nil {
panic(err)
}
return affected != 0
}
func ChangeTopicFavoriteCount(topicId int, num int) bool {
affected, err := adapter.Engine.ID(topicId).Incr("favorite_count", num).Update(Topic{})
if err != nil {
panic(err)
}
return affected != 0
}
func ChangeTopicSubscribeCount(topicId int, num int) bool {
affected, err := adapter.Engine.ID(topicId).Incr("subscribe_count", num).Update(Topic{})
if err != nil {
panic(err)
}
return affected != 0
}
func ChangeTopicReplyCount(topicId int, num int) bool {
affected, err := adapter.Engine.ID(topicId).Incr("reply_count", num).Update(Topic{})
if err != nil {
panic(err)
}
return affected != 0
}
func ChangeTopicLastReplyUser(topicId int, memberId string, updateTime string) bool {
topic := GetTopic(topicId)
if topic == nil {
return false
}
topic.LastReplyUser = memberId
topic.LastReplyTime = updateTime
if len(memberId) == 0 {
topic.LastReplyTime = ""
}
affected, err := adapter.Engine.Id(topicId).Cols("last_reply_user, last_reply_time").Update(topic)
if err != nil {
panic(err)
}
return affected != 0
}
func GetTopicsWithTab(tab string, limit, offset int) []*TopicWithAvatar {
if tab == "all" {
topics := GetTopics(limit, offset)
return topics
} else {
topics := []*Topic{}
err := adapter.Engine.Table("topic").
Where("tab_id = ?", tab).And("deleted = ?", 0).And("is_hidden = ?", 0).
Desc("tab_top_time", "last_reply_time").
Cols("id, author, node_id, node_name, title, created_time, last_reply_user, last_Reply_time, reply_count, favorite_count, deleted, home_page_top_time, tab_top_time, node_top_time").
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
return getAvataredTopics(topics)
}
}
func UpdateTopicHotInfo(topicId string, hot int) bool {
topic := new(Topic)
topic.Hot = hot
affected, err := adapter.Engine.Id(topicId).Cols("hot").Update(topic)
if err != nil {
panic(err)
}
return affected != 0
}
func GetHotTopic(limit int) []*TopicWithAvatar {
var topics []*Topic
err := adapter.Engine.Table("topic").
Where("deleted = ? ", 0).
Desc("hot").
Cols("id, author, node_id, node_name, title, created_time, last_reply_user, last_Reply_time, reply_count, favorite_count, deleted, home_page_top_time, tab_top_time, node_top_time").
Limit(limit).Find(&topics)
if err != nil {
panic(err)
}
return getAvataredTopics(topics)
}
// GetSortedTopics *sort: 1 means Asc, 2 means Desc, 0 means no effect.
func GetSortedTopics(lastReplySort, hotSort, favCountSort, createdTimeSort string, limit int, offset int) []*TopicWithAvatar {
var topics []*Topic
db := adapter.Engine.Table("topic")
// last reply time sort
switch lastReplySort {
case "1":
db = db.Asc("last_reply_time")
case "2":
db = db.Desc("last_reply_time")
}
// hot sort
switch hotSort {
case "1":
db = db.Asc("hot")
case "2":
db = db.Desc("hot")
}
// favorite count sort
switch favCountSort {
case "1":
db = db.Asc("favorite_count")
case "2":
db = db.Desc("favorite_count")
}
// created time sort
switch createdTimeSort {
case "1":
db = db.Desc("created_time")
case "2":
db = db.Desc("created_time")
}
err := db.
Where("deleted = ? and is_hidden <> ?", 0, 1).
Limit(limit, offset).Find(&topics)
if err != nil {
panic(err)
}
return getAvataredTopics(topics)
}
func GetTopicEditableStatus(user *casdoorsdk.User, author, nodeId, createdTime string) bool {
if CheckIsAdmin(user) || CheckNodeModerator(user, nodeId) {
return true
}
if GetUserName(user) != author {
return false
}
t, err := time.Parse("2006-01-02T15:04:05+08:00", createdTime)
if err != nil {
return false
}
h, _ := time.ParseDuration("-1h")
t = t.Add(8 * h)
now := time.Now()
if now.Sub(t).Minutes() > TopicEditableTime {
return false
}
return true
}
// ChangeTopicTopExpiredTime changes topic's top expired time.
// topType: tab, node or homePage.
func ChangeTopicTopExpiredTime(id int, date, topType string) bool {
topic := GetTopic(id)
if topic == nil {
return false
}
switch topType {
case "tab":
topic.TabTopTime = date
case "node":
topic.NodeTopTime = date
case "homePage":
topic.HomePageTopTime = date
}
affected, err := adapter.Engine.Id(id).Cols("tab_top_time, node_top_time, home_page_top_time").Update(topic)
if err != nil {
panic(err)
}
return affected != 0
}
// ExpireTopTopic searches and expires expired top topic.
func ExpireTopTopic() int {
topics := []*Topic{}
err := adapter.Engine.Where("tab_top_time != ?", "").Or("node_top_time != ?", "").Or("home_page_top_time != ?", "").Cols("id, tab_top_time, node_top_time, home_page_top_time").Find(&topics)
if err != nil {
panic(err)
}
var num int
date := util.GetCurrentTime()
for _, v := range topics {
if v.TabTopTime <= date {
res := ChangeTopicTopExpiredTime(v.Id, "", "tab")
if res {
num++
}
}
if v.NodeTopTime <= date {
res := ChangeTopicTopExpiredTime(v.Id, "", "node")
if res {
num++
}
}
if v.HomePageTopTime <= date {
res := ChangeTopicTopExpiredTime(v.Id, "", "homePage")
if res {
num++
}
}
}
return num
}
func (t Topic) GetAllRepliesOfTopic() []string {
var ret []string
var replies []Reply
err := adapter.Engine.Where("topic_id = ? and deleted = 0", t.Id).Find(&replies)
if err != nil {
panic(err)
}
var content string
for _, reply := range replies {
if reply.EditorType == "markdown" {
content = string(markdown.ToHTML([]byte(reply.Content), nil, nil))
} else {
content = reply.Content
}
ret = append(ret, content)
}
return ret
}
func SearchTopics(keyword string) []*TopicWithAvatar {
topics := []*Topic{}
sqlKeyword := fmt.Sprintf("%%%s%%", keyword)
err := adapter.Engine.Where("deleted = 0").Where("title like ? or content like ?", sqlKeyword, sqlKeyword).Find(&topics)
if err != nil {
panic(err)
}
topics2 := []*Topic{}
for _, topic := range topics {
content := RemoveHtmlTags(topic.Content)
if !strings.Contains(content, keyword) && !strings.Contains(topic.Title, keyword) {
continue
}
topics2 = append(topics2, topic)
}
return getAvataredTopics(topics2)
}

72
object/topic_test.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright 2022 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"fmt"
"strconv"
"testing"
)
func TestSyncTopicReplyCount(t *testing.T) {
InitConfig()
InitAdapter()
topics := GetAllTopics()
for _, topic := range topics {
num := GetTopicReplyNum(topic.Id)
if num != topic.ReplyCount {
tmp := topic.ReplyCount
topic.ReplyCount = num
UpdateTopic(topic.Id, topic)
fmt.Printf("[update topic:%d]: ReplyCount: %d -> %d\n", topic.Id, tmp, topic.ReplyCount)
}
}
fmt.Println("Synced ReplyCount of all topics!")
}
func TestSyncTopicFavoriteCount(t *testing.T) {
InitConfig()
InitAdapter()
topics := GetAllTopics()
for _, topic := range topics {
num := GetTopicFavoritesNum(strconv.Itoa(topic.Id))
if num != topic.FavoriteCount {
tmp := topic.FavoriteCount
topic.FavoriteCount = num
UpdateTopic(topic.Id, topic)
fmt.Printf("[update topic:%d]: FavoriteCount: %d -> %d\n", topic.Id, tmp, topic.FavoriteCount)
}
}
fmt.Println("Synced FavoriteCount of all topics!")
}
func TestSyncTopicSubscribeCount(t *testing.T) {
InitConfig()
InitAdapter()
Topics := GetAllTopics()
for _, topic := range Topics {
num := GetTopicSubscribeNum(strconv.Itoa(topic.Id))
if num != topic.SubscribeCount {
tmp := topic.SubscribeCount
topic.SubscribeCount = num
UpdateTopic(topic.Id, topic)
fmt.Printf("[update topic:%d]: SubscribeCount: %d -> %d\n", topic.Id, tmp, topic.SubscribeCount)
}
}
fmt.Println("Synced SubscribeCount of all topics!")
}

174
object/translator.go Normal file
View File

@ -0,0 +1,174 @@
package object
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"regexp"
)
type Translator struct {
Id string `xorm:"varchar(50) notnull pk" json:"id"`
Name string `xorm:"varchar(50)" json:"name"`
Translator string `xorm:"varchar(50)" json:"translator"`
Key string `xorm:"varchar(200)" json:"key"`
Enable bool `xorm:"bool" json:"enable"`
Visible bool `xorm:"bool" json:"visible"`
}
type TranslateData struct {
SrcLang string `json:"srcLang"`
Target string `json:"target"`
ErrMsg string `json:"err_msg"`
}
type GoogleTranslationResult struct {
Data struct {
Translations []struct {
TranslatedText string `json:"translatedText"`
DetectedSourceLanguage string `json:"detectedSourceLanguage"`
} `json:"translations"`
} `json:"data"`
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Errors []struct {
Message string `json:"message"`
Domain string `json:"domain"`
Reason string `json:"reason"`
} `json:"errors"`
} `json:"error"`
}
func StrTranslate(srcStr, targetLang string) *TranslateData {
replaceStr := "<code>RplaceWithCasnodeTranslator<code/>"
contentReg := regexp.MustCompile(`(?s)\x60{1,3}[^\x60](.*?)\x60{1,3}`)
translateReg := regexp.MustCompile(replaceStr)
translateData := &TranslateData{}
translator := GetEnableTranslator()
if translator == nil || !translator.Visible {
translateData.ErrMsg = "Translate Failed"
return translateData
}
codeBlocks := contentReg.FindAllString(srcStr, -1)
var cbList []string
if codeBlocks != nil {
for _, cbItem := range codeBlocks {
cbList = append(cbList, cbItem)
}
}
srcStr = contentReg.ReplaceAllString(srcStr, replaceStr)
params := url.Values{
"target": {targetLang},
"format": {"text"},
"key": {translator.Key},
"q": {srcStr},
}
resp, _ := http.PostForm("https://translation.googleapis.com/language/translate/v2", params)
defer resp.Body.Close()
respByte, _ := ioutil.ReadAll(resp.Body)
var translateResp GoogleTranslationResult
translateResp.Error.Code = 0
err := json.Unmarshal(respByte, &translateResp)
if err != nil {
panic(err)
}
translateStr := translateResp.Data.Translations[0].TranslatedText
detectSrcLang := translateResp.Data.Translations[0].DetectedSourceLanguage
replacedCb := translateReg.FindAllString(translateStr, -1)
var replacedCbList []string
if replacedCb != nil {
for _, replacedCbItem := range replacedCb {
replacedCbList = append(replacedCbList, replacedCbItem)
}
}
if len(replacedCbList) != len(codeBlocks) {
translateData.ErrMsg = "Translate Failed"
return translateData
}
replaceIndex := 0
translateStr = translateReg.ReplaceAllStringFunc(translateStr, func(src string) string {
replaceIndex = replaceIndex + 1
return cbList[replaceIndex-1]
})
if translateResp.Error.Code != 0 {
translateData.ErrMsg = translateResp.Error.Message
} else {
translateData.SrcLang = detectSrcLang
translateData.Target = translateStr
}
return translateData
}
func AddTranslator(translator Translator) bool {
affected, err := adapter.Engine.Insert(translator)
if err != nil {
panic(err)
}
return affected != 0
}
func GetTranslator(id string) *[]Translator {
translators := []Translator{}
var err error
if id != "" {
err = adapter.Engine.Where("id = ?", id).Find(&translators)
} else {
err = adapter.Engine.Find(&translators)
}
if err != nil {
panic(err)
}
return &translators
}
func GetEnableTranslator() *Translator {
var translator Translator
resultNum, err := adapter.Engine.Where("enable = ?", true).Get(&translator)
if err != nil {
panic(err)
}
if resultNum {
return &translator
}
return nil
}
func UpdateTranslator(translator Translator) bool {
_, err := adapter.Engine.Where("enable = ?", true).Cols("enable").Update(Translator{Enable: false})
if err != nil {
return false
}
affected, err := adapter.Engine.Id(translator.Id).AllCols().Update(translator)
if err != nil {
panic(err)
}
return affected != 0
}
func DelTranslator(id string) bool {
affected, err := adapter.Engine.Where("id = ?", id).Delete(&Translator{})
if err != nil {
panic(err)
}
return affected != 0
}

136
object/type.go Normal file
View File

@ -0,0 +1,136 @@
// Copyright 2020 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import "github.com/casdoor/casdoor-go-sdk/casdoorsdk"
type LatestReply struct {
TopicId int `xorm:"id" json:"topicId"`
NodeId string `json:"nodeId"`
NodeName string `json:"nodeName"`
Author string `json:"author"`
ReplyContent string `xorm:"content" json:"replyContent"`
TopicTitle string `xorm:"title" json:"topicTitle"`
ReplyTime string `xorm:"created_time" json:"replyTime"`
TopicAuthor string `xorm:"author" json:"topicAuthor"`
}
type TopicWithAvatar struct {
Topic `xorm:"extends"`
Avatar string `json:"avatar"`
ThanksStatus bool `json:"thanksStatus"`
Editable bool `json:"editable"`
NodeModerator bool `json:"nodeModerator"`
}
type NodeTopic struct {
Topic `xorm:"extends"`
Avatar string `json:"avatar"`
ThanksStatus bool `json:"thanksStatus"`
Editable bool `json:"editable"`
NodeModerator bool `json:"nodeModerator"`
ContentLength int `json:"contentLength"`
}
type ReplyWithAvatar struct {
Reply `xorm:"extends"`
Avatar string `json:"avatar"`
ThanksStatus bool `json:"thanksStatus"`
Deletable bool `json:"deletable"`
Editable bool `json:"editable"`
ConsumptionAmount int `xorm:"amount" json:"amount"`
Child []*ReplyWithAvatar `json:"child"`
}
type NodeFavoritesRes struct {
NodeInfo *Node `json:"nodeInfo"`
TopicNum int `json:"topicNum"`
}
type CommunityHealth struct {
Member int `json:"member"`
Topic int `json:"topic"`
Reply int `json:"reply"`
}
type NodeRelation struct {
ParentNode *Node `json:"parentNode"`
RelatedNode []*Node `json:"relatedNode"`
ChildNode []*Node `json:"childNode"`
}
type NotificationResponse struct {
*Notification `xorm:"extends"`
Title string `json:"title"`
Content string `json:"content"`
Avatar string `json:"avatar"`
}
type NodeNavigationResponse struct {
*Tab
Nodes []*Node `json:"nodes"`
}
type PlaneWithNodes struct {
*Plane
Nodes []*Node `json:"nodes"`
}
type BalanceResponse struct {
Amount int `json:"amount"`
Title string `json:"title"`
Length int `json:"length"`
Balance int `json:"balance"`
ObjectId int `json:"objectId"`
ReceiverId string `json:"receiverId"`
ConsumerId string `json:"consumerId"`
CreatedTime string `json:"createdTime"`
ConsumptionType int `json:"consumptionType"`
}
type AdminTabInfo struct {
Id string `json:"id"`
Name string `json:"name"`
Sorter int `json:"sorter"`
CreatedTime string `json:"createdTime"`
DefaultNode string `json:"defaultNode"`
HomePage bool `json:"homePage"`
NodesNum int `json:"nodesNum"`
TopicsNum int `json:"topicsNum"`
}
type AdminMemberInfo struct {
casdoorsdk.User
FileQuota int `json:"fileQuota"`
FileUploadNum int `json:"fileUploadNum"`
Status int `json:"status"`
TopicNum int `json:"topicNum"`
ReplyNum int `json:"replyNum"`
LatestLogin string `json:"latestLogin"`
Score int `json:"score"`
}
type AdminPlaneInfo struct {
Plane
Sorter int `json:"sorter"`
Visible bool `json:"visible"`
NodesNum int `json:"nodesNum"`
Nodes []*Node `json:"nodes"`
}
type AdminTopicInfo struct {
Topic
Deleted bool `json:"deleted"`
}

51
object/util.go Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"strconv"
"github.com/astaxie/beego"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
func GetUserField(user *casdoorsdk.User, field string) string {
return user.Properties[field]
}
func GetUserFieldInt(user *casdoorsdk.User, field string) int {
res, err := strconv.Atoi(user.Properties[field])
if err != nil {
panic(err)
}
return res
}
func SetUserField(user *casdoorsdk.User, field string, value string) {
if user.Properties == nil {
user.Properties = map[string]string{}
}
user.Properties[field] = value
}
func getInitScore() int {
score, err := strconv.Atoi(beego.AppConfig.String("initScore"))
if err != nil {
panic(err)
}
return score
}

View File

@ -0,0 +1,45 @@
// Copyright 2021 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package routers
import (
"github.com/astaxie/beego/context"
"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
)
func AutoSigninFilter(ctx *context.Context) {
//if getSessionUser(ctx) != "" {
// return
//}
// "/page?access_token=123"
accessToken := ctx.Input.Query("accessToken")
if accessToken == "signout" {
// sign out
setSessionClaims(ctx, nil)
return
}
if accessToken != "" {
claims, err := casdoorsdk.ParseJwtToken(accessToken)
if err != nil {
responseError(ctx, "invalid JWT token")
return
}
claims.AccessToken = accessToken
setSessionClaims(ctx, claims)
}
}

Some files were not shown because too many files have changed in this diff Show More