5 Commits

10 changed files with 144 additions and 241 deletions

View File

@@ -1,26 +1,9 @@
# Contributing to WriteFreely
# Contributing to WriteFreely (lupine's fork)
Welcome! We're glad you're interested in contributing to the WriteFreely project.
This is a fork of https://github.com/writeas/writefreely
To start, we'd suggest checking out [our Phabricator board](https://phabricator.write.as/tag/write_freely/) to see where the project is at and where it's going. You can also [join the WriteFreely forums](https://discuss.write.as/c/writefreely) to start talking about what you'd like to do or see.
You probably want to contribute there instead of here.
## Asking Questions
The best place to get answers to your questions is on [our forums](https://discuss.write.as/c/writefreely). You can quickly log in using your GitHub account and ask the community about anything. We're also there to answer your questions and discuss potential changes or features.
## Submitting Bugs
Please use the [GitHub issue tracker](https://github.com/writeas/writefreely/issues/new) to report any bugs you encounter. We're very responsive there and try to keep open issues to a minimum, so you can help by:
* **Only reporting bugs in the issue tracker**
* Providing as much information as possible to replicate the issue, including server logs around the incident
* Including the `[app]` section of your configuration, if related
* Breaking issues into smaller pieces if they're larger or have many parts
## Contributing code
We gladly welcome development help, regardless of coding experience. We can also use help [translating the app](https://poeditor.com/join/project/TIZ6HFRFdE) and documenting it!
**Before writing or submitting any code**, please sign our [contributor's agreement](https://phabricator.write.as/L1) so we can accept your contributions. It is substantially similar to the _Apache Individual Contributor License Agreement_. If you'd like to know about the rationale behind this requirement, you can [read more about that here](https://phabricator.write.as/w/writefreely/cla/).
Once you've done that, please feel free to [submit a pull request](https://github.com/writeas/writefreely/pulls) for any small improvements. For larger projects, please [join our development discussions](https://discuss.write.as/c/writefreely) or [get in touch](https://write.as/contact) so we can talk about what you'd like to work on.
Note that I haven't signed their CLA, which means they won't accept my code. So
that's why this is a fork. I'm not doing this to be a pain, I just don't want to
run a mysql server on the limited hardware I have at home.

View File

@@ -1,30 +1,10 @@
 
<p align="center">
<a href="https://writefreely.org"><img src="https://writefreely.org/writefreely.svg" width="350px" alt="Write Freely" /></a>
</p>
<hr />
<p align="center">
<a href="https://github.com/writeas/writefreely/releases/">
<img src="https://img.shields.io/github/release/writeas/writefreely.svg" alt="Latest release" />
</a>
<a href="https://goreportcard.com/report/github.com/writeas/writefreely">
<img src="https://goreportcard.com/badge/github.com/writeas/writefreely" alt="Go Report Card" />
</a>
<a href="https://travis-ci.org/writeas/writefreely">
<img src="https://travis-ci.org/writeas/writefreely.svg" alt="Build status" />
</a>
</p>
&nbsp;
# WriteFreely (lupine's fork)
WriteFreely is a beautifully pared-down blogging platform that's simple on the surface, yet powerful underneath.
It's designed to be flexible and share your writing widely, so it's built around plain text and can publish to the _fediverse_ via ActivityPub. It's easy to install and lightweight.
**[Start a blog on our instance](https://write.as/new/blog/federated)**
[Try the editor](https://write.as/new)
[Find another instance](https://writefreely.org/instances)
See [CONTRIBUTING.md](contributing.md) for more information on why it's forked
## Features
@@ -38,16 +18,17 @@ It's designed to be flexible and share your writing widely, so it's built around
> **Note** this is currently alpha software. We're quickly moving out of this v0.x stage, but while we're in it, there are no guarantees that this is ready for production use.
First, download the [latest release](https://github.com/writeas/writefreely/releases/latest) for your OS. It includes everything you need to start your blog.
First, build writefreely.
Now extract the files from the archive, change into the directory, and do the following steps:
Now change into the directory, and do the following steps:
```bash
# 1) Log into MySQL and run:
# CREATE DATABASE writefreely;
# 1) Start an SQLite3 database
sqlite3 writefreely.sqlite3
#
# 2) Import the schema with:
mysql -u YOURUSERNAME -p writefreely < schema.sql
.read schema.sql
# 3) Configure your blog
./writefreely --config

View File

@@ -7,7 +7,6 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
"github.com/writeas/activity/streams"
"github.com/writeas/httpsig"
@@ -344,12 +343,10 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// Add follower locally, since it wasn't found before
res, err := t.Exec("INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)", fullActor.ID, fullActor.Inbox, fullActor.Endpoints.SharedInbox)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
return
}
if isConstraintError(err) {
t.Rollback()
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
return
} else {
t.Rollback()
log.Error("Couldn't add new remoteuser in DB: %v\n", err)
@@ -367,12 +364,10 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
// Add in key
_, err = t.Exec("INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)", fullActor.PublicKey.ID, followerID, fullActor.PublicKey.PublicKeyPEM)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
return
}
if isConstraintError(err) {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
return
} else {
t.Rollback()
log.Error("Couldn't add follower keys in DB: %v\n", err)
@@ -382,14 +377,12 @@ func handleFetchCollectionInbox(app *app, w http.ResponseWriter, r *http.Request
}
// Add follow
_, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, NOW())", c.ID, followerID)
_, err = t.Exec("INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, "+sqlNow+")", c.ID, followerID)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number != mySQLErrDuplicateKey {
t.Rollback()
log.Error("Couldn't add follower in DB: %v\n", err)
return
}
if isConstraintError(err) {
t.Rollback()
log.Error("Couldn't add follower in DB: %v\n", err)
return
} else {
t.Rollback()
log.Error("Couldn't add follower in DB: %v\n", err)

21
app.go
View File

@@ -4,7 +4,7 @@ import (
"database/sql"
"flag"
"fmt"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"html/template"
"net/http"
"os"
@@ -203,15 +203,9 @@ func Serve() {
app.formDecoder.RegisterConverter(sql.NullFloat64{}, converter.ConvertSQLNullFloat64)
// Check database configuration
if app.cfg.Database.User == "" || app.cfg.Database.Password == "" {
log.Error("Database user or password not set.")
os.Exit(1)
}
if app.cfg.Database.Host == "" {
app.cfg.Database.Host = "localhost"
}
if app.cfg.Database.Database == "" {
app.cfg.Database.Database = "writeas"
log.Error("Database filename not set.")
os.Exit(1)
}
connectToDatabase(app)
@@ -249,7 +243,7 @@ func Serve() {
http.Handle("/", r)
log.Info("Serving on http://localhost:%d\n", app.cfg.Server.Port)
log.Info("---")
err = http.ListenAndServe(fmt.Sprintf(":%d", app.cfg.Server.Port), nil)
err = http.ListenAndServe(fmt.Sprintf("localhost:%d", app.cfg.Server.Port), nil)
if err != nil {
log.Error("Unable to start: %v", err)
os.Exit(1)
@@ -258,7 +252,12 @@ func Serve() {
func connectToDatabase(app *app) {
log.Info("Connecting to database...")
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true", app.cfg.Database.User, app.cfg.Database.Password, app.cfg.Database.Host, app.cfg.Database.Port, app.cfg.Database.Database))
if app.cfg.Database.Type != "sqlite3" {
log.Error("This fork of writefreely only supports sqlite3 databases")
os.Exit(1)
}
db, err := sql.Open("sqlite3", app.cfg.Database.Database)
if err != nil {
log.Error("%s", err)
os.Exit(1)

View File

@@ -18,11 +18,7 @@ type (
DatabaseCfg struct {
Type string `ini:"type"`
User string `ini:"username"`
Password string `ini:"password"`
Database string `ini:"database"`
Host string `ini:"host"`
Port int `ini:"port"`
}
AppCfg struct {
@@ -59,9 +55,8 @@ func New() *Config {
Port: 8080,
},
Database: DatabaseCfg{
Type: "mysql",
Host: "localhost",
Port: 3306,
Type: "sqlite3",
Database: "writefreely.sqlite3",
},
App: AppCfg{
Host: "http://localhost:8080",

View File

@@ -65,30 +65,7 @@ func Configure() (*SetupData, error) {
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Username",
Validate: validateNonEmpty,
Default: data.Config.Database.User,
}
data.Config.Database.User, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Password",
Validate: validateNonEmpty,
Default: data.Config.Database.Password,
Mask: '*',
}
data.Config.Database.Password, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Database name",
Label: "Database filename",
Validate: validateNonEmpty,
Default: data.Config.Database.Database,
}
@@ -97,29 +74,6 @@ func Configure() (*SetupData, error) {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Host",
Validate: validateNonEmpty,
Default: data.Config.Database.Host,
}
data.Config.Database.Host, err = prompt.Run()
if err != nil {
return data, err
}
prompt = promptui.Prompt{
Templates: tmpls,
Label: "Port",
Validate: validatePort,
Default: fmt.Sprintf("%d", data.Config.Database.Port),
}
dbPort, err := prompt.Run()
if err != nil {
return data, err
}
data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number
fmt.Println()
title(" App setup ")
fmt.Println()

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/mattn/go-sqlite3"
"github.com/guregu/null"
"github.com/guregu/null/zero"
uuid "github.com/nu7hatch/gouuid"
@@ -22,10 +22,19 @@ import (
"github.com/writeas/writefreely/author"
)
const (
mySQLErrDuplicateKey = 1062
var (
sqlNow = `datetime('now')`
sqlExpires = `(expires IS NULL OR expires > `+sqlNow+`)`
)
func isConstraintError(err error) bool {
if sqliteErr, ok := err.(*sqlite3.Error); ok {
return sqliteErr.Code == sqlite3.ErrConstraint
}
return false
}
type writestore interface {
CreateUser(*User, string) error
UpdateUserEmail(keys *keychain, userID int64, email string) error
@@ -110,13 +119,11 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
// 1. Add to `users` table
// NOTE: Assumes User's Password is already hashed!
res, err := t.Exec("INSERT INTO users (username, password, email, created) VALUES (?, ?, ?, NOW())", u.Username, u.HashedPass, u.Email)
res, err := t.Exec("INSERT INTO users (username, password, email, created) VALUES (?, ?, ?, "+sqlNow+")", u.Username, u.HashedPass, u.Email)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Rolling back users INSERT: %v\n", err)
@@ -136,10 +143,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error {
res, err = t.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", u.Username, collectionTitle, "", CollUnlisted, u.ID, 0)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Rolling back collections INSERT: %v\n", err)
return err
@@ -208,10 +213,8 @@ func (db *datastore) CreateCollection(alias, title string, userID int64) (*Colle
// All good, so create new collection
res, err := db.Exec("INSERT INTO collections (alias, title, description, privacy, owner_id, view_count) VALUES (?, ?, ?, ?, ?, ?)", alias, title, "", CollUnlisted, userID, 0)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
}
if isConstraintError(err) {
return nil, impart.HTTPError{http.StatusConflict, "Collection already exists."}
}
log.Error("Couldn't add to collections: %v\n", err)
return nil, err
@@ -329,7 +332,7 @@ func (db *datastore) GetUserNameFromToken(accessToken string) (string, error) {
var oneTime bool
var username string
err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&username, &oneTime)
err := db.QueryRow("SELECT username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND "+sqlExpires, t).Scan(&username, &oneTime)
switch {
case err == sql.ErrNoRows:
return "", ErrBadAccessToken
@@ -354,7 +357,7 @@ func (db *datastore) GetUserDataFromToken(accessToken string) (int64, string, er
var userID int64
var oneTime bool
var username string
err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&userID, &username, &oneTime)
err := db.QueryRow("SELECT user_id, username, one_time FROM accesstokens LEFT JOIN users ON user_id = id WHERE token = ? AND "+sqlExpires, t).Scan(&userID, &username, &oneTime)
switch {
case err == sql.ErrNoRows:
return 0, "", ErrBadAccessToken
@@ -393,7 +396,7 @@ func (db *datastore) GetUserIDPrivilege(accessToken string) (userID int64, sudo
}
var oneTime bool
err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token = ? AND (expires IS NULL OR expires > NOW())", t).Scan(&userID, &sudo, &oneTime)
err := db.QueryRow("SELECT user_id, sudo, one_time FROM accesstokens WHERE token = ? AND "+sqlExpires, t).Scan(&userID, &sudo, &oneTime)
switch {
case err == sql.ErrNoRows:
return -1, false
@@ -425,7 +428,7 @@ func (db *datastore) DeleteToken(accessToken []byte) error {
// userID.
func (db *datastore) FetchLastAccessToken(userID int64) string {
var t []byte
err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND (expires IS NULL OR expires > NOW()) ORDER BY created DESC LIMIT 1", userID).Scan(&t)
err := db.QueryRow("SELECT token FROM accesstokens WHERE user_id = ? AND "+sqlExpires+" ORDER BY created DESC LIMIT 1", userID).Scan(&t)
switch {
case err == sql.ErrNoRows:
return ""
@@ -468,12 +471,16 @@ func (db *datastore) GetTemporaryOneTimeAccessToken(userID int64, validSecs int,
// Insert UUID to `accesstokens`
binTok := u[:]
expirationVal := "NULL"
now := time.Now()
expirationVal := interface{}("NULL")
if validSecs > 0 {
expirationVal = fmt.Sprintf("DATE_ADD(NOW(), INTERVAL %d SECOND)", validSecs)
expirationVal = interface{}(now.Add(time.Duration(validSecs)*time.Second))
}
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, created, one_time, expires) VALUES (?, ?, NOW(), ?, "+expirationVal+")", string(binTok), userID, oneTime)
_, err = db.Exec("INSERT INTO accesstokens (token, user_id, created, one_time, expires) VALUES (?, ?, ?, ?, ?)", string(binTok), userID, now, oneTime, expirationVal)
if err != nil {
log.Error("Couldn't INSERT accesstoken: %v", err)
return "", err
@@ -557,18 +564,16 @@ func (db *datastore) CreatePost(userID, collID int64, post *SubmittedPost) (*Pos
}
}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, "+sqlNow+", ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
// Duplicate entry error; try a new slug
// TODO: make this a little more robust
// TODO: reuse exact same db.Exec statement as above
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
}
if isConstraintError(err) {
// Duplicate entry error; try a new slug
// TODO: make this a little more robust
// TODO: reuse exact same db.Exec statement as above
slug = sql.NullString{id.GenSafeUniqueSlug(slug.String), true}
_, err = db.Exec("INSERT INTO posts (id, slug, title, content, text_appearance, language, rtl, owner_id, collection_id, updated, view_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, "+sqlNow+", ?)", friendlyID, slug, post.Title, post.Content, appearance, post.Language, post.IsRTL, ownerID, ownerCollID, 0)
if err != nil {
return nil, handleFailedPostInsert(fmt.Errorf("Retried slug generation, still failed: %v", err))
} else {
return nil, handleFailedPostInsert(err)
}
@@ -650,7 +655,7 @@ func (db *datastore) UpdateOwnedPost(post *AuthenticatedPost, userID int64) erro
return ErrPostNoUpdatableVals
}
queryUpdates += sep + "updated = NOW()"
queryUpdates += sep + "updated = " + sqlNow
res, err := db.Exec("UPDATE posts SET "+queryUpdates+" WHERE id = ? AND "+authCondition, params...)
if err != nil {
@@ -819,7 +824,7 @@ func (db *datastore) UpdateCollection(c *SubmittedCollection, alias string) erro
return nil
}
const postCols = "id, slug, text_appearance, language, rtl, privacy, owner_id, collection_id, pinned_position, created, updated, view_count, title, content"
const postCols = "id, slug, text_appearance, language, rtl, owner_id, collection_id, pinned_position, created, updated, view_count, title, content"
// getEditablePost returns a PublicPost with the given ID only if the given
// edit token is valid for the post.
@@ -830,7 +835,7 @@ func (db *datastore) GetEditablePost(id, editToken string) (*PublicPost, error)
p := &Post{}
row := db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE id = ? LIMIT 1", id)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName)
switch {
case err == sql.ErrNoRows:
return nil, ErrPostNotFound
@@ -877,7 +882,7 @@ func (db *datastore) GetPost(id string, collectionID int64) (*PublicPost, error)
where = "id = ?"
}
row = db.QueryRow("SELECT "+postCols+", (SELECT username FROM users WHERE users.id = posts.owner_id) AS username FROM posts WHERE "+where+" LIMIT 1", params...)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content, &ownerName)
switch {
case err == sql.ErrNoRows:
if collectionID > 0 {
@@ -909,7 +914,7 @@ func (db *datastore) GetOwnedPost(id string, ownerID int64) (*PublicPost, error)
where := "id = ? AND owner_id = ?"
params := []interface{}{id, ownerID}
row = db.QueryRow("SELECT "+postCols+" FROM posts WHERE "+where+" LIMIT 1", params...)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
err := row.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
switch {
case err == sql.ErrNoRows:
return nil, ErrPostNotFound
@@ -962,7 +967,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) {
var count int64
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition, c.ID).Scan(&count)
switch {
@@ -1001,7 +1006,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture bool) (*[]P
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND pinned_position IS NULL "+timeCondition+" ORDER BY created "+order+limitStr, collID)
if err != nil {
@@ -1014,7 +1019,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture bool) (*[]P
posts := []PublicPost{}
for rows.Next() {
p := &Post{}
err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
if err != nil {
log.Error("Failed scanning row: %v", err)
break
@@ -1058,7 +1063,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include
}
timeCondition := ""
if !includeFuture {
timeCondition = "AND created <= NOW()"
timeCondition = "AND created <= " + sqlNow
}
rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND LOWER(content) RLIKE ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, "#"+strings.ToLower(tag)+"[[:>:]]")
if err != nil {
@@ -1071,7 +1076,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include
posts := []PublicPost{}
for rows.Next() {
p := &Post{}
err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content)
if err != nil {
log.Error("Failed scanning row: %v", err)
break
@@ -1133,17 +1138,16 @@ func (db *datastore) CanCollect(cpr *ClaimPostRequest, userID int64) bool {
func (db *datastore) AttemptClaim(p *ClaimPostRequest, query string, params []interface{}, slugIdx int) (sql.Result, error) {
qRes, err := db.Exec(query, params...)
if err != nil {
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey && slugIdx > -1 {
s := id.GenSafeUniqueSlug(p.Slug)
if s == p.Slug {
// Sanity check to prevent infinite recursion
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
}
p.Slug = s
params[slugIdx] = p.Slug
return db.AttemptClaim(p, query, params, slugIdx)
if isConstraintError(err) && slugIdx > -1 {
s := id.GenSafeUniqueSlug(p.Slug)
if s == p.Slug {
// Sanity check to prevent infinite recursion
return qRes, fmt.Errorf("GenSafeUniqueSlug generated nothing unique: %s", s)
}
p.Slug = s
params[slugIdx] = p.Slug
return db.AttemptClaim(p, query, params, slugIdx)
}
return qRes, fmt.Errorf("attemptClaim: %s", err)
}
@@ -1431,7 +1435,7 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 {
}
func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) {
rows, err := db.Query("SELECT id, slug, title, LEFT(content, 80), pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
rows, err := db.Query("SELECT id, slug, title, SUBSTR(content, 80), pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID)
if err != nil {
log.Error("Failed selecting pinned posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."}
@@ -1694,10 +1698,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
_, err = t.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, u.ID)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Unable to update users table: %v", err)
return ErrInternalGeneral
@@ -1706,10 +1708,8 @@ func (db *datastore) ChangeSettings(app *app, u *User, s *userSettings) error {
_, err = t.Exec("UPDATE collections SET alias = ? WHERE alias = ? AND owner_id = ?", newUsername, u.Username, u.ID)
if err != nil {
t.Rollback()
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mySQLErrDuplicateKey {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
if isConstraintError(err) {
return impart.HTTPError{http.StatusConflict, "Username is already taken."}
}
log.Error("Unable to update collection: %v", err)
return ErrInternalGeneral

View File

@@ -5,6 +5,7 @@ import (
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"strings"
"time"
)
type nodeInfoResolver struct {
@@ -67,6 +68,10 @@ func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) {
log.Error("Unable to fetch post counts: %v", err)
}
ago_1 := time.Now().Add(-(30*24*time.Hour))
ago_6 := time.Now().Add(-(30*24*time.Hour*6))
if r.cfg.App.PublicStats {
// Display bi-yearly / monthly stats
err = r.db.QueryRow(`SELECT COUNT(*) FROM (
@@ -75,7 +80,7 @@ FROM posts
INNER JOIN collections c
ON collection_id = c.id
WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`).Scan(&activeHalfYear)
AND updated > ? co`, ago_6).Scan(&activeHalfYear)
err = r.db.QueryRow(`SELECT COUNT(*) FROM (
SELECT DISTINCT collection_id
@@ -83,7 +88,7 @@ FROM posts
INNER JOIN FROM collections c
ON collection_id = c.id
WHERE collection_id IS NOT NULL
AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`).Scan(&activeMonth)
AND updated > ? co`, ago_1).Scan(&activeMonth)
}
return nodeinfo.Usage{

View File

@@ -80,7 +80,6 @@ type (
Font string `db:"text_appearance" json:"appearance"`
Language zero.String `db:"language" json:"language"`
RTL zero.Bool `db:"rtl" json:"rtl"`
Privacy int64 `db:"privacy" json:"-"`
OwnerID null.Int `db:"owner_id" json:"-"`
CollectionID null.Int `db:"collection_id" json:"-"`
PinnedPosition null.Int `db:"pinned_position" json:"-"`

View File

@@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS `accesstokens` (
`expires` datetime DEFAULT NULL,
`user_agent` varchar(255) NOT NULL,
PRIMARY KEY (`token`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -28,9 +28,9 @@ CREATE TABLE IF NOT EXISTS `accesstokens` (
CREATE TABLE IF NOT EXISTS `collectionattributes` (
`collection_id` int(6) NOT NULL,
`attribute` varchar(128) NOT NULL,
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`value` varchar(255) NOT NULL,
PRIMARY KEY (`collection_id`,`attribute`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS `collectionkeys` (
`public_key` blob NOT NULL,
`private_key` blob NOT NULL,
PRIMARY KEY (`collection_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS `collectionpasswords` (
`collection_id` int(6) NOT NULL,
`password` char(60) NOT NULL,
PRIMARY KEY (`collection_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -67,7 +67,7 @@ CREATE TABLE IF NOT EXISTS `collectionredirects` (
`prev_alias` varchar(100) NOT NULL,
`new_alias` varchar(100) NOT NULL,
PRIMARY KEY (`prev_alias`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -76,19 +76,18 @@ CREATE TABLE IF NOT EXISTS `collectionredirects` (
--
CREATE TABLE IF NOT EXISTS `collections` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`alias` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`description` varchar(160) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`id` integer PRIMARY KEY ASC,
`alias` varchar(100) DEFAULT NULL,
`title` varchar(255) NOT NULL,
`description` varchar(160) NOT NULL,
`style_sheet` text,
`script` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
`script` text,
`format` varchar(8) DEFAULT NULL,
`privacy` tinyint(1) NOT NULL,
`owner_id` int(6) NOT NULL,
`view_count` int(6) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `alias` (`alias`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CONSTRAINT `alias` UNIQUE (`alias`)
);
-- --------------------------------------------------------
@@ -103,20 +102,18 @@ CREATE TABLE IF NOT EXISTS `posts` (
`text_appearance` char(4) NOT NULL DEFAULT 'norm',
`language` char(2) DEFAULT NULL,
`rtl` tinyint(1) DEFAULT NULL,
`privacy` tinyint(1) NOT NULL,
`owner_id` int(6) DEFAULT NULL,
`collection_id` int(6) DEFAULT NULL,
`pinned_position` tinyint(1) UNSIGNED DEFAULT NULL,
`pinned_position` tinyint(1) DEFAULT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated` timestamp NOT NULL,
`view_count` int(6) NOT NULL,
`title` varchar(160) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`title` varchar(160) NOT NULL,
`content` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_slug` (`collection_id`,`slug`),
UNIQUE KEY `owner_id` (`owner_id`,`id`),
KEY `privacy_id` (`privacy`,`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CONSTRAINT `id_slug` UNIQUE (`collection_id`,`slug`),
CONSTRAINT `owner_id` UNIQUE (`owner_id`,`id`)
);
-- --------------------------------------------------------
@@ -129,7 +126,7 @@ CREATE TABLE IF NOT EXISTS `remotefollows` (
`remote_user_id` int(11) NOT NULL,
`created` datetime NOT NULL,
PRIMARY KEY (`collection_id`,`remote_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -138,12 +135,11 @@ CREATE TABLE IF NOT EXISTS `remotefollows` (
--
CREATE TABLE IF NOT EXISTS `remoteuserkeys` (
`id` varchar(255) NOT NULL,
`id` varchar(255) PRIMARY KEY NOT NULL,
`remote_user_id` int(11) NOT NULL,
`public_key` blob NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `follower_id` (`remote_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CONSTRAINT `follower_id` UNIQUE (`remote_user_id`)
);
-- --------------------------------------------------------
@@ -152,14 +148,13 @@ CREATE TABLE IF NOT EXISTS `remoteuserkeys` (
--
CREATE TABLE IF NOT EXISTS `remoteusers` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`id` integer PRIMARY KEY ASC,
`actor_id` varchar(255) NOT NULL,
`inbox` varchar(255) NOT NULL,
`shared_inbox` varchar(255) NOT NULL,
`followers` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `collection_id` (`actor_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CONSTRAINT `collection_id` UNIQUE (`actor_id`)
);
-- --------------------------------------------------------
@@ -168,11 +163,11 @@ CREATE TABLE IF NOT EXISTS `remoteusers` (
--
CREATE TABLE IF NOT EXISTS `userattributes` (
`user_id` int(6) NOT NULL,
`user_id` integer NOT NULL,
`attribute` varchar(64) NOT NULL,
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`value` varchar(255) NOT NULL,
PRIMARY KEY (`user_id`,`attribute`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
);
-- --------------------------------------------------------
@@ -181,11 +176,10 @@ CREATE TABLE IF NOT EXISTS `userattributes` (
--
CREATE TABLE IF NOT EXISTS `users` (
`id` int(6) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
`password` char(60) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
`id` integer PRIMARY KEY ASC,
`username` varchar(100) NOT NULL,
`password` char(60) NOT NULL,
`email` varbinary(255) DEFAULT NULL,
`created` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CONSTRAINT `username` UNIQUE (`username`)
);