cerca

lean forum software (pmc local branch)
Log | Files | Refs | README | LICENSE

commit e7542278dba645c2081666372d7d28e8a178dd3d
parent 224b4eda043c69f81cf53ab4fb3ea18518638aca
Author: cblgh <cblgh@cblgh.org>
Date:   Tue, 18 Oct 2022 15:20:25 +0200

restructuring

Diffstat:
Mdefaults/defaults.go | 2+-
Mdefaults/sample-about.md | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mdefaults/sample-config.toml | 16+++++++++-------
Mdefaults/sample-logo.svg | 7+++++++
Mdefaults/sample-rules.md | 1+
Mdefaults/sample-verification-instructions.md | 4++++
Mhtml/register.html | 10++++++----
Mi18n/i18n.go | 54+++++++++++-------------------------------------------
Mrun.go | 4+++-
Mserver/server.go | 65++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtypes/types.go | 5++---
Mutil/util.go | 19+++++++++++++++----
12 files changed, 154 insertions(+), 82 deletions(-)

diff --git a/defaults/defaults.go b/defaults/defaults.go @@ -8,7 +8,7 @@ import ( var DEFAULT_ABOUT string //go:embed sample-logo.svg -var DEFAULT_LOG string +var DEFAULT_LOGO string //go:embed sample-rules.md var DEFAULT_RULES string diff --git a/defaults/sample-about.md b/defaults/sample-about.md @@ -0,0 +1,49 @@ +# About +This forum is for and by the [Merveilles](https://wiki.xxiivv.com/site/merveilles.html) +community. + +The [forum software](https://github.com/cblgh/cerca) itself was created from scratch by +[cblgh](https://cblgh.org) at the start of 2022, after a long time of pining for a new wave of +forums hangs. + +If you are from Merveilles: [register](/register) an account. If you're a passerby, feel free +to read the [public threads](/). + +## Code of conduct +As with all Merveilles spaces, this forum abides by the compact set out in the [Mervilles Code +of Conduct](https://github.com/merveilles/Resources/blob/master/CONDUCT.md). + +## Forum syntax +Posts in the forum are made using [Markdown syntax](https://en.wikipedia.org/wiki/Markdown#Examples). + +<b>\*\*Bold text\*\*</b> and <i>\*italics\*</i> + +<ul> + <li>\* lists</li> + <li>\* like </li> + <li>\* this </li> +</ul> + +<blockquote>&gt; Blockquote</blockquote> + +<code>\`typewriter text\`</code> + +<!-- leave the <pre><code> blocks from reformatting! they render all their spacing :)--> +<pre><code>\`\`\` +blocks of +code like +this +\`\`\` +</code></pre> + +Create links like <code>\[this\]\(url\)</code>, and embed images like: <code>!\[description\]\(url\)</code>. Note how the image +syntax's exclamation mark precedes the regular link syntax. + +Each post in the thread can be referenced like <code>\[this post\]\(#12\)</code>, where 12 is the post number which can be +found at each post timestamp. + +<pre><code>this is one paragraph. +this belongs to the same paragraph. + +this is a new paragraph +</code></pre> diff --git a/defaults/sample-config.toml b/defaults/sample-config.toml @@ -2,14 +2,16 @@ name = "" link = "" conduct_url = "" +language = "" [theme] -background = "pink" -foreground = "red" -links = "aqua" +background = "" +foreground = "" +links = "" [documents] -logo = "" -about = "" -rules = "" -verification_explanation = "" +logo = "logo.svg" +about = "about.md" +rules = "rules.md" +verification_explanation = "verification-instructions.md" +custom_css = "" diff --git a/defaults/sample-logo.svg b/defaults/sample-logo.svg @@ -0,0 +1,6 @@ +<svg width="30" height="30" fill="black" stroke="none" viewBox="60 60 200 200" xmlns="http://www.w3.org/2000/svg"> + <g> + <path d="M185,65 A60,60 0 0,0 125,125 L185,125 Z M125,245 A60,60 0 0,0 185,185 L125,185 Z M95,125 A30,30 0 0,1 125,155 A30,30 0 0,1 95,185 A30,30 0 0,1 65,155 A30,30 0 0,1 95,125 Z M155,125 A30,30 0 0,1 185,155 A30,30 0 0,1 155,185 A30,30 0 0,1 125,155 A30,30 0 0,1 155,125 Z M215,125 A30,30 0 0,1 245,155 A30,30 0 0,1 215,185 A30,30 0 0,1 185,155 A30,30 0 0,1 215,125 "/> + <path d="M125,65 A60,60 0 0,1 185,125 L125,125 Z M185,245 A60,60 0 0,1 125,185 L185,185 Z M65,65 A60,60 0 0,1 125,125 L65,125 Z M65,245 A60,60 0 0,0 125,185 L65,185 Z M245,65 A60,60 0 0,0 185,125 L245,125 Z M245,245 A60,60 0 0,1 185,185 L245,185 Z"/> + </g> +</svg> +\ No newline at end of file diff --git a/defaults/sample-rules.md b/defaults/sample-rules.md @@ -0,0 +1 @@ +This forum is for the [Merveilles](https://wiki.xxiivv.com/site/merveilles.html) community. To register, you need to either belong to the <a href="https://webring.xxiivv.com">Merveilles Webring</a> or the <a href="https://merveilles.town">Merveilles Fediverse instance. diff --git a/defaults/sample-verification-instructions.md b/defaults/sample-verification-instructions.md @@ -0,0 +1,4 @@ +You can use either your mastodon profile or your webring site to verify your registration. + +* **Mastodon**: temporarily add a new metadata item to [your profile](https://merveilles.town/settings/profile) containing the verification code displayed above. Pass your profile as the verification link. +* **Webring site**: Upload a plaintext file somewhere on your webring domain (incl. subdomain) containing the verification code from above. Pass the link to the uploaded file as the verification link (make sure it is viewable in a browser). diff --git a/html/register.html b/html/register.html @@ -1,12 +1,12 @@ {{ template "head" . }} <main> <h1> {{ "Register" | translate | capitalize }}</h1> - <p>{{ "ForumDescription" | translateWithData | tohtml}} {{ "RegisterRules" | translateWithData | tohtml }}.</p> + {{ .Data.Rules }} <!-- registrationregistration rules will be inserted here from the rules document being read from the config --> <p>{{ "RegisterVerificationCode" | translate }} <b>{{ .Data.VerificationCode }}</b></p> <details> <summary> {{ "RegisterVerificationInstructionsTitle" | translate }}</summary> - <!-- add your communities verification instructions by changing the translations --> - {{ "RegisterVerificationInstructions" | translate | tohtml }} + <!-- add your communities verification instructions by editing the verification-instructions document, see the config for where to find it!--> + {{ .Data.VerificationInstructions }} </details> <form method="post"> @@ -18,6 +18,7 @@ <label for="verificationlink">{{ "RegisterVerificationLink" | translate }}: </label> <input type="text" required id="verification link" name="verificationlink"> <input type="hidden" name="verificationcode" value="{{.Data.VerificationCode}}"> + {{ if ne .Data.ConductLink "" }} <div> <div> <input type="checkbox" required id="coc"> @@ -25,9 +26,10 @@ </div> <div> <input type="checkbox" required id="coc2" > - <label style="display: inline;" for="coc2">{{ "RegisterConductCodeBoxTwo" | translate | tohtml }}</label> + <label style="display: inline;" for="coc2">{{ "RegisterConductCodeBoxTwo" | translateWithData | tohtml }}</label> </div> </div> + {{ end }} <input type="submit" value='{{ "Register" | translate | capitalize }}'> </form> diff --git a/i18n/i18n.go b/i18n/i18n.go @@ -26,7 +26,6 @@ var English = map[string]string{ "SortPostsRecent": "recent posts", "SortThreadsRecent": "most recent threads", - "ForumDescription": "This forum is for the <a href='{{ .CommunityLink }}'>{{.CommunityName}}</a> community.", "LoginNoAccount": "Don't have an account yet? <a href='/register'>Register</a> one.", "LoginFailure": "<b>Failed login attempt:</b> incorrect password, wrong username, or a non-existent user.", "LoginAlreadyLoggedIn": `You are already logged in. Would you like to <a href="/logout">log out</a>?`, @@ -90,19 +89,9 @@ var English = map[string]string{ "RegisterVerificationCode": "Your verification code is", "RegisterVerificationInstructionsTitle": "Verification instructions", - // TODO (2022-09-20): make verification instructions another md file to load, pass path from config - "RegisterVerificationInstructions": `<p>You can use either your mastodon profile or your webring site to verify your registration.</p> - <ul> - <li><b>Mastodon:</b> temporarily add a new metadata item to <a href="https://merveilles.town/settings/profile">your profile</a> containing the verification code - displayed above. Pass your profile as the verification link.</li> - <li><b>Webring site:</b> Upload a plaintext file somewhere on your webring domain (incl. subdomain) containing - the verification code from above. Pass the link to the uploaded file as the verification link (make sure it is viewable in a browser).</li> - </ul> - `, - "RegisterVerificationLink": "Verification link", - "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">{{ .CommunityName }} Code of Conduct</a>`, - "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">read it</a>`, + "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="{{ .Data.Link }}">{{ .Data.Name }} Code of Conduct</a>`, + "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="{{ .Data.Link }}">read it</a>`, "PasswordResetDescription": "On this page we'll go through a few steps to securely reset your password—without resorting to any emails!", "PasswordResetUsernameQuestion": "First up: what was your username?", @@ -140,7 +129,6 @@ var Swedish = map[string]string{ "SortRecentPosts": "nyast poster", "SortRecentThreads": "nyast trådar", - "ForumDescription": "Detta forum är till för <a href='{{ .CommunityLink }}'>{{.CommunityName}}</a> communityt.", "LoginNoAccount": "Saknar du konto? <a href='/register'>Skapa</a> ett.", "LoginFailure": "<b>Misslyckat inloggningsförsök:</b> inkorrekt lösenord, fel användernamn, eller obefintlig användare.", "LoginAlreadyLoggedIn": `Du är redan inloggad. Vill du <a href="/logout">logga ut</a>?`, @@ -204,19 +192,9 @@ var Swedish = map[string]string{ "RegisterVerificationCode": "Din verifikationskod är", "RegisterVerificationInstructionsTitle": "Verification instructions", - // TODO (2022-09-20): make verification instructions another md file to load, pass path from config - "RegisterVerificationInstructions": `<p>You can use either your mastodon profile or your webring site to verify your registration.</p> - <ul> - <li><b>Mastodon:</b> temporarily add a new metadata item to <a href="https://merveilles.town/settings/profile">your profile</a> containing the verification code - displayed above. Pass your profile as the verification link.</li> - <li><b>Webring site:</b> Upload a plaintext file somewhere on your webring domain (incl. subdomain) containing - the verification code from above. Pass the link to the uploaded file as the verification link (make sure it is viewable in a browser).</li> - </ul> - `, - "RegisterVerificationLink": "Verificationsnyckel", - "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">{{ .CommunityName }} Code of Conduct</a>`, - "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">read it</a>`, + "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="{{ .Data.Link }}">{{ .Data.Name }} Code of Conduct</a>`, + "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="{{ .Data.Link }}">read it</a>`, "PasswordResetDescription": "På denna sida går vi igenom ett par steg för att säkert nollställa ditt lösenord—utan att behöva ta till mejl!", "PasswordResetUsernameQuestion": "För de första: hur löd användarnamnet?", @@ -254,7 +232,6 @@ var EspanolMexicano = map[string]string{ "SortRecentPosts": "recent posts", "SortRecentThreads": "most recent threads", - "ForumDescription": "Este foro es principalmente para las personas de la comunidad <a href='{{ .CommunityLink }}'>{{ .CommunityName }}</a>.", "LoginNoAccount": "¿No tienes una cuenta? <a href='/register'>Registra</a> una. ", "LoginFailure": "<b>Failed login attempt:</b> incorrect password, wrong username, or a non-existent user.", "LoginAlreadyLoggedIn": `You are already logged in. Would you like to <a href="/logout">log out</a>?`, @@ -317,18 +294,9 @@ var EspanolMexicano = map[string]string{ "RegisterVerificationCode": "Your verification code is", "RegisterVerificationInstructionsTitle": "Verification instructions", - // TODO (2022-09-20): make verification instructions another md file to load, pass path from config - "RegisterVerificationInstructions": `<p>You can use either your mastodon profile or your webring site to verify your registration.</p> - <ul> - <li><b>Mastodon:</b> temporarily add a new metadata item to <a href="https://merveilles.town/settings/profile">your profile</a> containing the verification code - displayed above. Pass your profile as the verification link.</li> - <li><b>Webring site:</b> Upload a plaintext file somewhere on your webring domain (incl. subdomain) containing - the verification code from above. Pass the link to the uploaded file as the verification link (make sure it is viewable in a browser).</li> - </ul> - `, "RegisterVerificationLink": "Verification link", - "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">{{ .CommunityName }} Code of Conduct</a>`, - "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="https://github.com/merveilles/Resources/blob/master/CONDUCT.md">read it</a>`, + "RegisterConductCodeBoxOne": `I have refreshed my memory of the <a target="_blank" href="{{ .Data.Link }}">{{ .Data.Name }} Code of Conduct</a>`, + "RegisterConductCodeBoxTwo": `Yes, I have actually <a target="_blank" href="{{ .Data.Link }}">read it</a>`, "PasswordResetDescription": "On this page we'll go through a few steps to securely reset your password—without resorting to any emails!", "PasswordResetUsernameQuestion": "First up: what was your username?", @@ -356,12 +324,12 @@ var translations = map[string]map[string]string{ "Swedish": Swedish, } -type Community struct { - CommunityName string - CommunityLink string +type TranslationData struct { + Data interface{} } -func (tr *Translator) TranslateWithData(key string, data Community) string { +func (tr *Translator) TranslateWithData(key string, data TranslationData) string { + fmt.Println(key, data) phrase := translations[tr.Language][key] t, err := template.New(key).Parse(phrase) ed := util.Describe("i18n translation") @@ -373,7 +341,7 @@ func (tr *Translator) TranslateWithData(key string, data Community) string { } func (tr *Translator) Translate(key string) string { - var empty Community + var empty TranslationData return tr.TranslateWithData(key, empty) } diff --git a/run.go b/run.go @@ -37,11 +37,13 @@ func main() { var allowlistLocation string var sessionKey string var configPath string + var dataDir string var dev bool flag.BoolVar(&dev, "dev", false, "trigger development mode") flag.StringVar(&allowlistLocation, "allowlist", "", "domains which can be used to read verification codes from during registration") flag.StringVar(&sessionKey, "authkey", "", "session cookies authentication key") flag.StringVar(&configPath, "config", "cerca.toml", "config and settings file containing cerca's customizations") + flag.StringVar(&dataDir, "data", "./data", "directory where cerca will dump its files (database and customizable documents)") flag.Parse() if len(sessionKey) == 0 { complain("please pass a random session auth key with --authkey") @@ -51,5 +53,5 @@ func main() { allowlist := readAllowlist(allowlistLocation) allowlist = append(allowlist, "merveilles.town") config := util.ReadConfig(configPath) - server.Serve(allowlist, sessionKey, dev, config) + server.Serve(allowlist, sessionKey, dev, dataDir, config) } diff --git a/server/server.go b/server/server.go @@ -22,6 +22,7 @@ import ( "cerca/server/session" "cerca/types" "cerca/util" + "cerca/defaults" "github.com/carlmjohnson/requests" ) @@ -73,6 +74,9 @@ type GenericMessageData struct { type RegisterData struct { VerificationCode string ErrorMessage string + Rules template.HTML + VerificationInstructions template.HTML + ConductLink string } type RegisterSuccessData struct { @@ -93,6 +97,8 @@ type RequestHandler struct { db *database.DB session *session.Session allowlist []string // allowlist of domains valid for forum registration + files map[string][]byte + config types.Config } var developing bool @@ -127,9 +133,6 @@ func (h RequestHandler) IsLoggedIn(req *http.Request) (bool, int) { } var ( - translator = i18n.Init("Swedish") - community = i18n.Community{"Merveilles", "https://wiki.xxiivv.com/site/merveilles.html"} - templateFuncs = template.FuncMap{ "formatDateTime": func(t time.Time) string { return t.Format("2006-01-02 15:04:05") @@ -153,7 +156,14 @@ var ( return translator.Translate(key) }, "translateWithData": func(key string) string { - return translator.TranslateWithData(key, community) + data := struct{ + Name string + Link string + }{ + Name: config.Community.Name, + Link: config.Community.ConductLink, + } + return translator.TranslateWithData(key, i18n.TranslationData{data}) }, "capitalize": util.Capitalize, "tohtml": func(s string) template.HTML { @@ -494,11 +504,14 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request return } + rules := util.Markup(template.HTML(h.files["rules"])) + verification := util.Markup(template.HTML(h.files["verification-instructions"])) + conduct := h.config.Community.ConductLink var verificationCode string renderErr := func(errFmt string, args ...interface{}) { errMessage := fmt.Sprintf(errFmt, args...) fmt.Println(errMessage) - h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, errMessage}}) + h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, errMessage, rules, verification, conduct}}) } var err error @@ -508,14 +521,15 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request verificationCode, err = h.session.GetVerificationCode(req) // we had an error getting the verification code, generate a code and set it on the session if err != nil { - verificationCode = fmt.Sprintf("MRV%06d\n", crypto.GenerateVerificationCode()) + prefix := util.VerificationPrefix(h.config.Community.Name) + verificationCode = fmt.Sprintf("%s%06d\n", prefix, crypto.GenerateVerificationCode()) err = h.session.SaveVerificationCode(req, res, verificationCode) if err != nil { renderErr("Had troubles setting the verification code on session") return } } - h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, ""}}) + h.renderView(res, "register", TemplateData{Data: RegisterData{verificationCode, "", rules, verification, conduct}}) case "POST": verificationCode, err = h.session.GetVerificationCode(req) if err != nil { @@ -610,13 +624,7 @@ func (h RequestHandler) GenericRoute(res http.ResponseWriter, req *http.Request) func (h RequestHandler) AboutRoute(res http.ResponseWriter, req *http.Request) { loggedIn, _ := h.IsLoggedIn(req) - // TODO (2022-09-19): - // * make sure file exists - // * create function to output a prefilled version, using the Community name and CommunityLink - // * embed the prefilled version in the code using golang's goembed - b, err := os.ReadFile("./about.md") - util.Check(err, "about route: open about.md") - input := util.Markup(template.HTML(b)) + input := util.Markup(template.HTML(h.files["about"])) h.renderView(res, "about-template", TemplateData{Data: input, LoggedIn: loggedIn, Title: translator.Translate("About")}) } @@ -720,18 +728,18 @@ func (h RequestHandler) DeletePostRoute(res http.ResponseWriter, req *http.Reque http.Redirect(res, req, threadURL, http.StatusSeeOther) } -func Serve(allowlist []string, sessionKey string, isdev bool, conf types.Config) { +func Serve(allowlist []string, sessionKey string, isdev bool, dir string, conf types.Config) { port := ":8272" - dir := "./data/" config = conf if isdev { developing = true + // TODO (2022-10-18): don't overload passed in dir; just use --data instead dir = "./testdata/" port = ":8277" } - forum, err := NewServer(allowlist, sessionKey, dir) + forum, err := NewServer(allowlist, sessionKey, dir, conf) if err != nil { util.Check(err, "instantiate CercaForum") } @@ -751,6 +759,7 @@ func Serve(allowlist []string, sessionKey string, isdev bool, conf types.Config) type CercaForum struct { http.ServeMux Directory string + Files map[string][]byte } func (u *CercaForum) directory() string { @@ -765,21 +774,39 @@ func (u *CercaForum) directory() string { return u.Directory } +func (c *CercaForum) loadFile(key, filepath, defaultContent string) { + _, err := util.CreateIfNotExist(filepath, defaultContent) + util.Check(err, "create if not exist (%s) %s", key, filepath) + c.Files[key], err = os.ReadFile(filepath) + util.Check(err, "read %s", filepath) +} + // NewServer sets up a new CercaForum object. Always use this to initialize // new CercaForum objects. Pass the result to http.Serve() with your choice // of net.Listener. -func NewServer(allowlist []string, sessionKey, dir string) (*CercaForum, error) { +func NewServer(allowlist []string, sessionKey, dir string, conf types.Config) (*CercaForum, error) { s := &CercaForum{ ServeMux: http.ServeMux{}, Directory: dir, + Files: make(map[string][]byte), } dbpath := filepath.Join(s.directory(), "forum.db") db := database.InitDB(dbpath) + // TODO (2022-10-18): introduce step where if config document path is empty => config.Documents.<path> = + // filepath.Join(s.directory(), <name>) + + // load the documents specified in the config + // iff document doesn't exist, dump a default document where it should be and read that + s.loadFile("about", config.Documents.AboutPath, defaults.DEFAULT_ABOUT) + s.loadFile("rules", config.Documents.RegisterRulesPath, defaults.DEFAULT_RULES) + s.loadFile("verification-instructions", config.Documents.VerificationExplanationPath, defaults.DEFAULT_VERIFICATION) + s.loadFile("logo", config.Documents.LogoPath, defaults.DEFAULT_LOGO) + /* note: be careful with trailing slashes; go's default handler is a bit sensitive */ // TODO (2022-01-10): introduce middleware to make sure there is never an issue with trailing slashes - handler := RequestHandler{&db, session.New(sessionKey, developing), allowlist} + handler := RequestHandler{&db, session.New(sessionKey, developing), allowlist, s.Files, config} s.ServeMux.HandleFunc("/reset/", handler.ResetPasswordRoute) s.ServeMux.HandleFunc("/about", handler.AboutRoute) s.ServeMux.HandleFunc("/logout", handler.LogoutRoute) diff --git a/types/types.go b/types/types.go @@ -1,8 +1,6 @@ package types type Config struct { - // for internal use - Files map[string]string // use as: // config.Files["about"] -> about markdown // config.Files["rules"] -> rules explanation markdown @@ -10,8 +8,8 @@ type Config struct { Community struct { Name string `json:"name"` - Link string `json:"link"` ConductLink string `json:"conduct_url"` + Language string `json:"language"` } `json:"general"` Theme struct { @@ -25,6 +23,7 @@ type Config struct { AboutPath string `json:"about"` RegisterRulesPath string `json:"rules"` VerificationExplanationPath string `json:"verification_explanation"` + CustomCSSPath string `json:"custom_css"` } `json:"documents"` } diff --git a/util/util.go b/util/util.go @@ -3,6 +3,7 @@ package util import ( "bytes" "encoding/base64" + "regexp" "encoding/hex" "encoding/json" "errors" @@ -103,6 +104,16 @@ func SanitizeStringStrict(s string) string { return strictContentGuardian.Sanitize(s) } +func VerificationPrefix (name string) string { + pattern := regexp.MustCompile("A|E|O|U|I|Y") + upper := strings.ToUpper(name) + replaced := string(pattern.ReplaceAll([]byte(upper), []byte(""))) + if len(replaced) < 3 { + replaced += "XYZ" + } + return replaced[0:3] +} + func GetThreadSlug(threadid int, title string, threadLen int) string { return fmt.Sprintf("/thread/%d/%s-%d/", threadid, SanitizeURL(title), threadLen) } @@ -143,12 +154,12 @@ func Capitalize(s string) string { return strings.ToUpper(string(s[0])) + s[1:] } -func createIfNotExist(name, content string) (bool, error) { - _, err := os.Stat(name) +func CreateIfNotExist(filepath, content string) (bool, error) { + _, err := os.Stat(filepath) if err != nil { // if the file doesn't exist, create it if errors.Is(err, fs.ErrNotExist) { - err = os.WriteFile(name, []byte(content), 0777) + err = os.WriteFile(filepath, []byte(content), 0777) if err != nil { return false, err } @@ -166,7 +177,7 @@ func createIfNotExist(name, content string) (bool, error) { // * util.checkFileExists(path, mockContents) func ReadConfig(confpath string) types.Config { ed := Describe("config") - _, err := createIfNotExist(confpath, defaults.DEFAULT_CONFIG) + _, err := CreateIfNotExist(confpath, defaults.DEFAULT_CONFIG) ed.Check(err, "create default config") data, err := os.ReadFile(confpath)