Merge branch 'master' into absolute_fluoride

This commit is contained in:
r 2022-01-27 12:05:15 +00:00
commit b8c0133bcd
29 changed files with 214 additions and 263 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
bloat
database
bloat.def.conf

12
INSTALL
View file

@ -15,12 +15,12 @@ This will perform a system wide installation of bloat. By default, it will
install the binary in /usr/local/bin and data files in /usr/local/share/bloat.
You can change these paths by editing the Makefile.
3. Edit and copy the config file
Edit the generated config file to you liking and then copy it to the default
config location. Comments in the config file describe what each config value
does. For most cases, you only need to change the value of "client_website".
$ $EDITOR bloat.def.conf
# cp bloat.def.conf /etc/bloat.conf
3. Edit the config file
bloat looks for a file named bloat.conf in the working directory and
/etc/bloat in that order. You can also specify another file by using the -f
flag. Comments in the config file describe what each config value does. For
most cases, you only need to change the value of "client_website".
# $EDITOR /etc/bloat.conf
4. Create database directory
Create a directory to store session information. Optionally, create a user

View file

@ -14,17 +14,11 @@ SRC=main.go \
service/*.go \
util/*.go \
all: bloat bloat.def.conf
all: bloat
bloat: $(SRC) $(TMPL)
$(GO) build $(GOFLAGS) -o bloat main.go
bloat.def.conf:
sed -e "s%=database%=/var/bloat%g" \
-e "s%=templates%=$(SHAREPATH)/templates%g" \
-e "s%=static%=$(SHAREPATH)/static%g" \
< bloat.conf > bloat.def.conf
install: bloat
mkdir -p $(DESTDIR)$(BINPATH) \
$(DESTDIR)$(SHAREPATH)/templates \
@ -35,6 +29,10 @@ install: bloat
chmod 0644 $(DESTDIR)$(SHAREPATH)/templates/*
cp -r static/* $(DESTDIR)$(SHAREPATH)/static
chmod 0644 $(DESTDIR)$(SHAREPATH)/static/*
sed -e "s%=database%=/var/bloat%g" \
-e "s%=templates%=$(SHAREPATH)/templates%g" \
-e "s%=static%=$(SHAREPATH)/static%g" \
< bloat.conf > /etc/bloat.conf
uninstall:
rm -f $(DESTDIR)$(BINPATH)/bloat
@ -42,4 +40,3 @@ uninstall:
clean:
rm -f bloat
rm -f bloat.def.conf

4
README
View file

@ -15,11 +15,11 @@ Building and Installation:
Typing make will build the binary
$ make
Edit the provided config file. See the bloat.conf file for more details.
Edit the default config file. See the bloat.conf file for more details.
$ ed bloat.conf
Run the binary
$ ./bloat -f bloat.conf
$ ./bloat
You can now access the frontend at http://127.0.0.1:8080, which is the default
listen address. See the INSTALL file for more details.

View file

@ -108,21 +108,30 @@ func Parse(r io.Reader) (c *config, err error) {
return
}
func ParseFile(file string) (c *config, err error) {
func ParseFiles(files []string) (c *config, err error) {
var lastErr error
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return
lastErr = err
if os.IsNotExist(err) {
continue
}
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return
lastErr = err
return nil, err
}
if info.IsDir() {
return nil, errors.New("invalid config file")
continue
}
return Parse(f)
}
if lastErr == nil {
lastErr = errors.New("invalid config file")
}
return nil, lastErr
}

19
main.go
View file

@ -2,6 +2,7 @@ package main
import (
"errors"
"flag"
"fmt"
"log"
"net/http"
@ -17,7 +18,7 @@ import (
)
var (
configFile = "/etc/bloat.conf"
configFiles = []string{"bloat.conf", "/etc/bloat.conf"}
)
func errExit(err error) {
@ -26,19 +27,13 @@ func errExit(err error) {
}
func main() {
opts, _, err := util.Getopts(os.Args, "f:")
if err != nil {
errExit(err)
}
configFile := flag.String("f", "", "config file")
flag.Parse()
for _, opt := range opts {
switch opt.Option {
case 'f':
configFile = opt.Value
if len(*configFile) > 0 {
configFiles = []string{*configFile}
}
}
config, err := config.ParseFile(configFile)
config, err := config.ParseFiles(configFiles)
if err != nil {
errExit(err)
}

View file

@ -243,9 +243,13 @@ func (c *Client) AccountUnblock(ctx context.Context, id string) (*Relationship,
}
// AccountMute mute the account.
func (c *Client) AccountMute(ctx context.Context, id string) (*Relationship, error) {
func (c *Client) AccountMute(ctx context.Context, id string, notifications *bool) (*Relationship, error) {
params := url.Values{}
if notifications != nil {
params.Set("notifications", strconv.FormatBool(*notifications))
}
var relationship Relationship
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil)
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), params, &relationship, nil)
if err != nil {
return nil, err
}

View file

@ -3,12 +3,28 @@ package mastodon
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
)
type Error struct {
code int
err string
}
func (e Error) Error() string {
return e.err
}
func (e Error) IsAuthError() bool {
switch e.code {
case http.StatusForbidden, http.StatusUnauthorized:
return true
}
return false
}
// Base64EncodeFileName returns the base64 data URI format string of the file with the file name.
func Base64EncodeFileName(filename string) (string, error) {
file, err := os.Open(filename)
@ -51,5 +67,8 @@ func parseAPIError(prefix string, resp *http.Response) error {
errMsg = fmt.Sprintf("%s: %s", errMsg, e.Error)
}
return errors.New(errMsg)
return Error{
code: resp.StatusCode,
err: errMsg,
}
}

View file

@ -23,9 +23,12 @@ type Notification struct {
}
// GetNotifications return notifications.
func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, excludes []string) ([]*Notification, error) {
func (c *Client) GetNotifications(ctx context.Context, pg *Pagination, includes, excludes []string) ([]*Notification, error) {
var notifications []*Notification
params := url.Values{}
for _, include := range includes {
params.Add("include_types[]", include)
}
for _, exclude := range excludes {
params.Add("exclude_types[]", exclude)
}

View file

@ -19,6 +19,19 @@ type ReplyInfo struct {
Number int `json:"number"`
}
type CreatedAt struct {
time.Time
}
func (t *CreatedAt) UnmarshalJSON(d []byte) error {
// Special case to handle retweets from GNU Social
// which returns empty string ("") in created_at
if len(d) == 2 && string(d) == `""` {
return nil
}
return t.Time.UnmarshalJSON(d)
}
// Status is struct to hold status.
type Status struct {
ID string `json:"id"`
@ -29,7 +42,7 @@ type Status struct {
InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
Reblog *Status `json:"reblog"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
CreatedAt CreatedAt `json:"created_at"`
Emojis []Emoji `json:"emojis"`
RepliesCount int64 `json:"replies_count"`
ReblogsCount int64 `json:"reblogs_count"`

View file

@ -11,6 +11,7 @@ type Settings struct {
FluorideMode bool `json:"fluoride_mode"`
DarkMode bool `json:"dark_mode"`
AntiDopamineMode bool `json:"anti_dopamine_mode"`
HideUnsupportedNotifs bool `json:"hide_unsupported_notifs"`
CSS string `json:"css"`
}
@ -26,6 +27,7 @@ func NewSettings() *Settings {
FluorideMode: false,
DarkMode: false,
AntiDopamineMode: false,
HideUnsupportedNotifs: false,
CSS: "",
}
}

View file

@ -1,8 +1,8 @@
package renderer
import (
"fmt"
"io"
"regexp"
"strconv"
"strings"
"text/template"
@ -39,29 +39,28 @@ type TemplateData struct {
Ctx *Context
}
func emojiHTML(e mastodon.Emoji, height string) string {
return `<img class="emoji" src="` + e.URL + `" alt=":` + e.ShortCode + `:" title=":` + e.ShortCode + `:" height="` + height + `"/>`
}
func emojiFilter(content string, emojis []mastodon.Emoji) string {
var replacements []string
var r string
for _, e := range emojis {
r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"24\" />",
e.URL, e.ShortCode, e.ShortCode)
replacements = append(replacements, ":"+e.ShortCode+":", r)
replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "24"))
}
return strings.NewReplacer(replacements...).Replace(content)
}
func statusContentFilter(spoiler string, content string,
emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
var quoteRE = regexp.MustCompile("(?mU)(^|> *|\n)(&gt;.*)(<br|$)")
var replacements []string
var r string
func statusContentFilter(spoiler, content string, emojis []mastodon.Emoji, mentions []mastodon.Mention) string {
if len(spoiler) > 0 {
content = spoiler + "<br />" + content
content = spoiler + "<br/>" + content
}
content = quoteRE.ReplaceAllString(content, `$1<span class="quote">$2</span>$3`)
var replacements []string
for _, e := range emojis {
r = fmt.Sprintf("<img class=\"emoji\" src=\"%s\" alt=\":%s:\" title=\":%s:\" height=\"32\" />",
e.URL, e.ShortCode, e.ShortCode)
replacements = append(replacements, ":"+e.ShortCode+":", r)
replacements = append(replacements, ":"+e.ShortCode+":", emojiHTML(e, "32"))
}
for _, m := range mentions {
replacements = append(replacements, `"`+m.URL+`"`, `"/user/`+m.ID+`" title="@`+m.Acct+`"`)

View file

@ -114,7 +114,8 @@ func (s *service) ErrorPage(c *client, err error, retry bool) error {
var sessionErr bool
if err != nil {
errStr = err.Error()
if err == errInvalidSession || err == errInvalidCSRFToken {
if me, ok := err.(mastodon.Error); ok && me.IsAuthError() ||
err == errInvalidSession || err == errInvalidCSRFToken {
sessionErr = true
}
}
@ -417,18 +418,24 @@ func (s *service) NotificationPage(c *client, maxID string,
var nextLink string
var unreadCount int
var readID string
var excludes []string
var includes, excludes []string
var pg = mastodon.Pagination{
MaxID: maxID,
MinID: minID,
Limit: 20,
}
if c.s.Settings.HideUnsupportedNotifs {
// Explicitly include the supported types.
// For now, only Pleroma supports this option, Mastadon
// will simply ignore the unknown params.
includes = []string{"follow", "follow_request", "mention", "reblog", "favourite"}
}
if c.s.Settings.AntiDopamineMode {
excludes = []string{"follow", "favourite", "reblog"}
excludes = append(excludes, "follow", "favourite", "reblog")
}
notifications, err := c.GetNotifications(c.ctx, &pg, excludes)
notifications, err := c.GetNotifications(c.ctx, &pg, includes, excludes)
if err != nil {
return
}
@ -914,8 +921,8 @@ func (s *service) Reject(c *client, id string) (err error) {
return c.FollowRequestReject(c.ctx, id)
}
func (s *service) Mute(c *client, id string) (err error) {
_, err = c.AccountMute(c.ctx, id)
func (s *service) Mute(c *client, id string, notifications *bool) (err error) {
_, err = c.AccountMute(c.ctx, id, notifications)
return
}

View file

@ -415,7 +415,13 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
mute := handle(func(c *client) error {
id, _ := mux.Vars(c.r)["id"]
err := s.Mute(c, id)
q := c.r.URL.Query()
var notifications *bool
if r, ok := q["notifications"]; ok && len(r) > 0 {
notifications = new(bool)
*notifications = r[0] == "true"
}
err := s.Mute(c, id, notifications)
if err != nil {
return err
}
@ -484,6 +490,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
fluorideMode := c.r.FormValue("fluoride_mode") == "true"
darkMode := c.r.FormValue("dark_mode") == "true"
antiDopamineMode := c.r.FormValue("anti_dopamine_mode") == "true"
hideUnsupportedNotifs := c.r.FormValue("hide_unsupported_notifs") == "true"
css := c.r.FormValue("css")
settings := &model.Settings{
@ -497,6 +504,7 @@ func NewHandler(s *service, logger *log.Logger, staticDir string) http.Handler {
FluorideMode: fluorideMode,
DarkMode: darkMode,
AntiDopamineMode: antiDopamineMode,
HideUnsupportedNotifs: hideUnsupportedNotifs,
CSS: css,
}

View file

@ -298,20 +298,24 @@ function setPos(el, cx, cy, mw, mh) {
}
var imgPrev = null;
var imgX = 0;
var imgY = 0;
function handleImgPreview(a) {
a.onmouseenter = function(e) {
var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24;
imgX = e.clientX;
imgY = e.clientY;
var img = document.createElement("img");
img.id = "img-preview";
img.src = e.target.getAttribute("href");
img.style["max-width"] = mw + "px";
img.style["max-height"] = mh + "px";
imgPrev = img;
img.onload = function(e2) {
setPos(e2.target, e.clientX, e.clientY, mw, mh);
setPos(imgPrev, imgX, imgY, mw, mh);
}
document.body.appendChild(img);
imgPrev = img;
}
a.onmouseleave = function(e) {
var img = document.getElementById("img-preview");
@ -324,7 +328,9 @@ function handleImgPreview(a) {
return;
var mw = document.documentElement.clientWidth;
var mh = document.documentElement.clientHeight - 24;
setPos(imgPrev, e.clientX, e.clientY, mw, mh);
imgX = e.clientX;
imgY = e.clientY;
setPos(imgPrev, imgX, imgY, mw, mh);
}
}

View file

@ -517,18 +517,18 @@ img.emoji {
margin-top: 6px;
}
.notification-title-container {
.page-title-container {
margin: 8px 0;
}
.page-refresh {
margin-right: 8px;
}
.notification-text {
vertical-align: middle;
}
.notification-refresh {
margin-right: 8px;
}
.notification-read {
display: inline-block;
}
@ -575,6 +575,10 @@ kbd {
position: fixed;
}
.quote {
color: #789922;
}
.dark {
background-color: #222222;
background-image: none;

View file

@ -94,7 +94,7 @@
<td> <kbd>C</kbd> </td>
</tr>
<tr>
<td> Refresh thread page </td>
<td> Refresh timeline/thread page </td>
<td> <kbd>T</kbd> </td>
</tr>
</table>

View file

@ -8,7 +8,7 @@
</div>
<div class="user-info-details-container">
<div class="user-info-details-name">
<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .User.DisplayName) .User.Emojis}} </bdi>
<a class="nav-link" href="/user/{{.User.ID}}" accesskey="0" title="User profile (0)">
<span class="status-uname"> @{{.User.Acct}} </span>
</a>

View file

@ -1,13 +1,13 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="notification-title-container">
<div class="page-title-container">
<span class="page-title">
Notifications
{{if and (not $.Ctx.AntiDopamineMode) (gt .UnreadCount 0)}}
({{.UnreadCount }})
{{end}}
</span>
<a class="notification-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
<a class="page-refresh" href="/notifications" target="_self" accesskey="R" title="Refresh (R)">refresh</a>
{{if .ReadID}}
<form class="notification-read" action="/notifications/read?max_id={{.ReadID}}" method="post" target="_self">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
@ -28,7 +28,7 @@
</div>
<div class="notification-follow">
<div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<span class="notification-text"> followed you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span>
@ -48,7 +48,7 @@
</div>
<div class="notification-follow">
<div class="notification-info-text">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<span class="notification-text"> wants to follow you -
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">{{TimeSince .CreatedAt}}</time>
</span>

View file

@ -9,7 +9,7 @@
</div>
<div class="user-list-name">
<div>
<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div>
<div class="status-dname"> {{EmojiFilter (html .DisplayName) .Emojis}} </div>
<a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div>
</a>

View file

@ -61,6 +61,11 @@
value="true" {{if .Settings.AntiDopamineMode}}checked{{end}}>
<label for="anti-dopamine-mode"> Enable <abbr title="Remove like/retweet/unread notification count and disable like/retweet/follow notifications">anti-dopamine mode</abbr> </label>
</div>
<div class="settings-form-field">
<input id="hide-unsupported-notifs" name="hide_unsupported_notifs" type="checkbox"
value="true" {{if .Settings.HideUnsupportedNotifs}}checked{{end}}>
<label for="hide-unsupported-notifs"> Hide unsupported notifications </label>
</div>
<div class="settings-form-field">
<input id="dark-mode" name="dark_mode" type="checkbox" value="true" {{if .Settings.DarkMode}}checked{{end}}>
<label for="dark-mode"> Use dark theme </label>

View file

@ -5,7 +5,7 @@
<a class="img-link" href="/user/{{.Account.ID}}">
<img class="status-profile-img" src="{{.Account.Avatar}}" title="@{{.Account.Acct}}" alt="avatar" height="24" />
</a>
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span>
</a>
@ -23,7 +23,7 @@
</div>
<div class="status">
<div class="status-name">
<bdi class="status-dname"> {{EmojiFilter .Account.DisplayName .Account.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .Account.DisplayName) .Account.Emojis}} </bdi>
<a href="/user/{{.Account.ID}}">
<span class="status-uname"> @{{.Account.Acct}} </span>
</a>
@ -227,8 +227,8 @@
<div class="status-action status-action-last">
<a class="status-time" href="{{if not .ShowReplies}}/thread/{{.ID}}{{end}}#status-{{.ID}}"
{{if $.Ctx.ThreadInNewTab}}target="_blank"{{end}}>
<time datetime="{{FormatTimeRFC3339 .CreatedAt}}" title="{{FormatTimeRFC822 .CreatedAt}}">
{{TimeSince .CreatedAt}}
<time datetime="{{FormatTimeRFC3339 .CreatedAt.Time}}" title="{{FormatTimeRFC822 .CreatedAt.Time}}">
{{TimeSince .CreatedAt.Time}}
</time>
</a>
</div>

View file

@ -1,8 +1,8 @@
{{with $s := .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="notification-title-container">
<div class="page-title-container">
<span class="page-title"> Thread </span>
<a class="notification-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
<a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
</div>
{{range .Statuses}}

View file

@ -1,6 +1,9 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> {{.Title}} </div>
<div class="page-title-container">
<span class="page-title"> {{.Title}} </span>
<a class="page-refresh" href="{{$.Ctx.Referrer}}" accesskey="T" title="Refresh (T)">refresh</a>
</div>
{{if eq .Type "remote"}}
<form class="search-form" action="/timeline/remote" method="GET">

View file

@ -11,7 +11,7 @@
</div>
<div class="user-profile-details-container">
<div>
<bdi class="status-dname"> {{EmojiFilter .User.DisplayName .User.Emojis}} </bdi>
<bdi class="status-dname"> {{EmojiFilter (html .User.DisplayName) .User.Emojis}} </bdi>
<span class="status-uname"> @{{.User.Acct}} </span>
<a class="remote-link" href="{{.User.URL}}" target="_blank" title="remote profile">
source
@ -83,6 +83,12 @@
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="mute" class="btn-link">
</form>
-
<form class="d-inline" action="/mute/{{.User.ID}}?notifications=false" method="post">
<input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
<input type="hidden" name="referrer" value="{{$.Ctx.Referrer}}">
<input type="submit" value="mute (keep notifications)" class="btn-link">
</form>
{{end}}
{{if .User.Pleroma.Relationship.Following}}
-

View file

@ -8,7 +8,7 @@
</a>
</div>
<div class="user-list-name">
<div class="status-dname"> {{EmojiFilter .DisplayName .Emojis}} </div>
<div class="status-dname"> {{EmojiFilter (html .DisplayName) .Emojis}} </div>
<a class="img-link" href="/user/{{.ID}}">
<div class="status-uname"> @{{.Acct}} </div>
</a>

View file

@ -1,6 +1,6 @@
{{with .Data}}
{{template "header.tmpl" (WithContext .CommonData $.Ctx)}}
<div class="page-title"> Search {{EmojiFilter .User.DisplayName .User.Emojis}}'s statuses </div>
<div class="page-title"> Search {{EmojiFilter (html .User.DisplayName) .User.Emojis}}'s statuses </div>
<form class="search-form" action="/usersearch/{{.User.ID}}" method="GET">
<span class="post-form-field>

View file

@ -1,122 +0,0 @@
/*
Copyright 2019 Drew DeVault <sir@cmpwn.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package util
import (
"fmt"
"os"
)
// In the case of "-o example", Option is 'o' and "example" is Value. For
// options which do not take an argument, Value is "".
type Option struct {
Option rune
Value string
}
// This is returned when an unknown option is found in argv, but not in the
// option spec.
type UnknownOptionError rune
func (e UnknownOptionError) Error() string {
return fmt.Sprintf("%s: unknown option -%c", os.Args[0], rune(e))
}
// This is returned when an option with a mandatory argument is missing that
// argument.
type MissingOptionError rune
func (e MissingOptionError) Error() string {
return fmt.Sprintf("%s: expected argument for -%c", os.Args[0], rune(e))
}
// Getopts implements a POSIX-compatible options interface.
//
// Returns a slice of options and the index of the first non-option argument.
//
// If an error is returned, you must print it to stderr to be POSIX complaint.
func Getopts(argv []string, spec string) ([]Option, int, error) {
optmap := make(map[rune]bool)
runes := []rune(spec)
for i, rn := range spec {
if rn == ':' {
if i == 0 {
continue
}
optmap[runes[i-1]] = true
} else {
optmap[rn] = false
}
}
var (
i int
opts []Option
)
for i = 1; i < len(argv); i++ {
arg := argv[i]
runes = []rune(arg)
if len(arg) == 0 || arg == "-" {
break
}
if arg[0] != '-' {
break
}
if arg == "--" {
i++
break
}
for j, opt := range runes[1:] {
if optopt, ok := optmap[opt]; !ok {
opts = append(opts, Option{'?', ""})
return opts, i, UnknownOptionError(opt)
} else if optopt {
if j+1 < len(runes)-1 {
opts = append(opts, Option{opt, string(runes[j+2:])})
break
} else {
if i+1 >= len(argv) {
if len(spec) >= 1 && spec[0] == ':' {
opts = append(opts, Option{':', string(opt)})
} else {
return opts, i, MissingOptionError(opt)
}
} else {
opts = append(opts, Option{opt, argv[i+1]})
i++
}
}
} else {
opts = append(opts, Option{opt, ""})
}
}
}
return opts, i, nil
}

View file

@ -2,24 +2,18 @@ package util
import (
"crypto/rand"
"math/big"
"encoding/base64"
)
var (
runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
runes_length = len(runes)
)
var enc = base64.URLEncoding
func NewRandID(n int) (string, error) {
data := make([]rune, n)
for i := range data {
num, err := rand.Int(rand.Reader, big.NewInt(int64(runes_length)))
data := make([]byte, enc.DecodedLen(n))
_, err := rand.Read(data)
if err != nil {
return "", err
}
data[i] = runes[num.Int64()]
}
return string(data), nil
return enc.EncodeToString(data), nil
}
func NewSessionID() (string, error) {