<commit_before>package service

import (
	"time"

	"github.com/juju/errors"
	"github.com/juju/loggo"
	"github.com/juju/utils"

	"github.com/juju/juju/service/common"
	"github.com/juju/juju/service/systemd"
	"github.com/juju/juju/service/upstart"
	"github.com/juju/juju/service/windows"
	"github.com/juju/juju/version"
)

var (
	logger = loggo.GetLogger("juju.mongo")
)

// These are the names of the init systems regognized by juju.
const (
	InitSystemWindows = "windows"
	InitSystemUpstart = "upstart"
	InitSystemSystemd = "systemd"
)

var _ Service = (*upstart.Service)(nil)
var _ Service = (*windows.Service)(nil)

type ServiceActions interface {
	// Start will try to start the service.
	Start() error

	// Stop will try to stop the service.
	Stop() error

	// Install installs a service.
	Install() error

	// Remove will remove the service.
	Remove() error
}

// TODO(ericsnow) bug #1426461
// Running, Installed, and Exists should return errors.

type ServiceStatus interface {
	// Running returns a boolean value that denotes
	// whether or not the service is running.
	Running() bool

	// Exists returns whether the service configuration exists in the
	// init directory with the same content that this Service would have
	// if installed.
	Exists() bool

	// Installed will return a boolean value that denotes
	// whether or not the service is installed.
	Installed() bool
}

// Service represents a service in the init system running on a host.
type Service interface {
	ServiceActions
	ServiceStatus

	// Name returns the service's name.
	Name() string

	// Conf returns the service's conf data.
	Conf() common.Conf

	// UpdateConfig adds a config to the service, overwriting the current one.
	UpdateConfig(conf common.Conf)

	// TODO(ericsnow) Eliminate StopAndRemove.

	// StopAndRemove will stop the service and remove it.
	StopAndRemove() error

	// TODO(ericsnow) Move all the commands into a separate interface.

	// InstallCommands returns the list of commands to run on a
	// (remote) host to install the service.
	InstallCommands() ([]string, error)

	// StartCommands returns the list of commands to run on a
	// (remote) host to start the service.
	StartCommands() ([]string, error)
}

// TODO(ericsnow) bug #1426458
// Eliminate the need to pass an empty conf for most service methods
// and several helper functions.

// NewService returns a new Service based on the provided info.
func NewService(name string, conf common.Conf, initSystem string) (Service, error) {
	switch initSystem {
	case InitSystemWindows:
		return windows.NewService(name, conf), nil
	case InitSystemUpstart:
		return upstart.NewService(name, conf), nil
	case InitSystemSystemd:
		svc, err := systemd.NewService(name, conf)
		if err != nil {
			return nil, errors.Trace(err)
		}
		return svc, nil
	default:
		return nil, errors.NotFoundf("init system %q", initSystem)
	}
}

// ListServices lists all installed services on the running system
func ListServices(initDir string) ([]string, error) {
	initName, ok := VersionInitSystem(version.Current)
	if !ok {
		return nil, errors.NotFoundf("init system on local host")
	}

	switch initName {
	case InitSystemWindows:
		services, err := windows.ListServices()
		if err != nil {
			return nil, err
		}
		return services, nil
	case InitSystemUpstart:
		services, err := upstart.ListServices(initDir)
		if err != nil {
			return nil, err
		}
		return services, nil
	case InitSystemSystemd:
		services, err := systemd.ListServices()
		if err != nil {
			return nil, err
		}
		return services, nil
	default:
		return nil, errors.NotFoundf("init system %q", initName)
	}
}

// ListServicesCommand returns the command that should be run to get
// a list of service names on a host.
func ListServicesCommand() string {
	return newShellSelectCommand(listServicesCommand)
}

func listServicesCommand(initSystem string) (string, bool) {
	switch initSystem {
	case InitSystemWindows:
		return windows.ListCommand(), true
	case InitSystemUpstart:
		return upstart.ListCommand(), true
	case InitSystemSystemd:
		return systemd.ListCommand(), true
	default:
		return "", false
	}
}

// InstallStartRetryAttempts defines how much InstallAndStart retries
// upon Start failures.
var InstallStartRetryAttempts = utils.AttemptStrategy{
	Total: 1 * time.Second,
	Delay: 250 * time.Millisecond,
}

// InstallAndStart installs the provided service and tries starting it.
// The first few Start failures are ignored.
func InstallAndStart(svc ServiceActions) error {
	if err := svc.Install(); err != nil {
		return errors.Trace(err)
	}

	// On slower disks, the init system may take a short time to realise
	// that there is a service there.
	var err error
	for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); {
		if err = svc.Start(); err == nil {
			break
		}
	}
	return errors.Trace(err)
}
<commit_msg>Move status methods back into Service.<commit_after>package service

import (
	"time"

	"github.com/juju/errors"
	"github.com/juju/loggo"
	"github.com/juju/utils"

	"github.com/juju/juju/service/common"
	"github.com/juju/juju/service/systemd"
	"github.com/juju/juju/service/upstart"
	"github.com/juju/juju/service/windows"
	"github.com/juju/juju/version"
)

var (
	logger = loggo.GetLogger("juju.mongo")
)

// These are the names of the init systems regognized by juju.
const (
	InitSystemWindows = "windows"
	InitSystemUpstart = "upstart"
	InitSystemSystemd = "systemd"
)

var _ Service = (*upstart.Service)(nil)
var _ Service = (*windows.Service)(nil)

type ServiceActions interface {
	// Start will try to start the service.
	Start() error

	// Stop will try to stop the service.
	Stop() error

	// Install installs a service.
	Install() error

	// Remove will remove the service.
	Remove() error
}

// Service represents a service in the init system running on a host.
type Service interface {
	ServiceActions

	// Name returns the service's name.
	Name() string

	// Conf returns the service's conf data.
	Conf() common.Conf

	// UpdateConfig adds a config to the service, overwriting the current one.
	UpdateConfig(conf common.Conf)

	// TODO(ericsnow) bug #1426461
	// Running, Installed, and Exists should return errors.

	// Running returns a boolean value that denotes
	// whether or not the service is running.
	Running() bool

	// Exists returns whether the service configuration exists in the
	// init directory with the same content that this Service would have
	// if installed.
	Exists() bool

	// Installed will return a boolean value that denotes
	// whether or not the service is installed.
	Installed() bool

	// TODO(ericsnow) Eliminate StopAndRemove.

	// StopAndRemove will stop the service and remove it.
	StopAndRemove() error

	// TODO(ericsnow) Move all the commands into a separate interface.

	// InstallCommands returns the list of commands to run on a
	// (remote) host to install the service.
	InstallCommands() ([]string, error)

	// StartCommands returns the list of commands to run on a
	// (remote) host to start the service.
	StartCommands() ([]string, error)
}

// TODO(ericsnow) bug #1426458
// Eliminate the need to pass an empty conf for most service methods
// and several helper functions.

// NewService returns a new Service based on the provided info.
func NewService(name string, conf common.Conf, initSystem string) (Service, error) {
	switch initSystem {
	case InitSystemWindows:
		return windows.NewService(name, conf), nil
	case InitSystemUpstart:
		return upstart.NewService(name, conf), nil
	case InitSystemSystemd:
		svc, err := systemd.NewService(name, conf)
		if err != nil {
			return nil, errors.Trace(err)
		}
		return svc, nil
	default:
		return nil, errors.NotFoundf("init system %q", initSystem)
	}
}

// ListServices lists all installed services on the running system
func ListServices(initDir string) ([]string, error) {
	initName, ok := VersionInitSystem(version.Current)
	if !ok {
		return nil, errors.NotFoundf("init system on local host")
	}

	switch initName {
	case InitSystemWindows:
		services, err := windows.ListServices()
		if err != nil {
			return nil, err
		}
		return services, nil
	case InitSystemUpstart:
		services, err := upstart.ListServices(initDir)
		if err != nil {
			return nil, err
		}
		return services, nil
	case InitSystemSystemd:
		services, err := systemd.ListServices()
		if err != nil {
			return nil, err
		}
		return services, nil
	default:
		return nil, errors.NotFoundf("init system %q", initName)
	}
}

// ListServicesCommand returns the command that should be run to get
// a list of service names on a host.
func ListServicesCommand() string {
	return newShellSelectCommand(listServicesCommand)
}

func listServicesCommand(initSystem string) (string, bool) {
	switch initSystem {
	case InitSystemWindows:
		return windows.ListCommand(), true
	case InitSystemUpstart:
		return upstart.ListCommand(), true
	case InitSystemSystemd:
		return systemd.ListCommand(), true
	default:
		return "", false
	}
}

// InstallStartRetryAttempts defines how much InstallAndStart retries
// upon Start failures.
var InstallStartRetryAttempts = utils.AttemptStrategy{
	Total: 1 * time.Second,
	Delay: 250 * time.Millisecond,
}

// InstallAndStart installs the provided service and tries starting it.
// The first few Start failures are ignored.
func InstallAndStart(svc ServiceActions) error {
	if err := svc.Install(); err != nil {
		return errors.Trace(err)
	}

	// On slower disks, the init system may take a short time to realise
	// that there is a service there.
	var err error
	for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); {
		if err = svc.Start(); err == nil {
			break
		}
	}
	return errors.Trace(err)
}
<|endoftext|>


<commit_before>package basichost

import (
	"context"
	"sync"

	goprocess "github.com/jbenet/goprocess"
	lgbl "github.com/libp2p/go-libp2p-loggables"
	inat "github.com/libp2p/go-libp2p-nat"
	inet "github.com/libp2p/go-libp2p-net"
	pstore "github.com/libp2p/go-libp2p-peerstore"
	ma "github.com/multiformats/go-multiaddr"
)

// A simple interface to manage NAT devices.
type NATManager interface {

	// Get the NAT device managed by the NAT manager.
	NAT() *inat.NAT

	// Receive a notification when the NAT device is ready for use.
	Ready() <-chan struct{}

	// Close all resources associated with a NAT manager.
	Close() error
}

// Create a NAT manager.
func NewNATManager(net inet.Network) NATManager {
	return newNatManager(net)
}

// natManager takes care of adding + removing port mappings to the nat.
// Initialized with the host if it has a NATPortMap option enabled.
// natManager receives signals from the network, and check on nat mappings:
//  * natManager listens to the network and adds or closes port mappings
//    as the network signals Listen() or ListenClose().
//  * closing the natManager closes the nat and its mappings.
type natManager struct {
	net   inet.Network
	natmu sync.RWMutex // guards nat (ready could obviate this mutex, but safety first.)
	nat   *inat.NAT

	ready chan struct{}     // closed once the nat is ready to process port mappings
	proc  goprocess.Process // natManager has a process + children. can be closed.
}

func newNatManager(net inet.Network) *natManager {
	nmgr := &natManager{
		net:   net,
		ready: make(chan struct{}),
	}

	nmgr.proc = goprocess.WithTeardown(func() error {
		// on closing, unregister from network notifications.
		net.StopNotify((*nmgrNetNotifiee)(nmgr))
		return nil
	})

	// discover the nat.
	nmgr.discoverNAT()
	return nmgr
}

// Close closes the natManager, closing the underlying nat
// and unregistering from network events.
func (nmgr *natManager) Close() error {
	return nmgr.proc.Close()
}

// Ready returns a channel which will be closed when the NAT has been found
// and is ready to be used, or the search process is done.
func (nmgr *natManager) Ready() <-chan struct{} {
	return nmgr.ready
}

func (nmgr *natManager) discoverNAT() {

	nmgr.proc.Go(func(worker goprocess.Process) {
		// inat.DiscoverNAT blocks until the nat is found or a timeout
		// is reached. we unfortunately cannot specify timeouts-- the
		// library we're using just blocks.
		//
		// Note: on early shutdown, there may be a case where we're trying
		// to close before DiscoverNAT() returns. Since we cant cancel it
		// (library) we can choose to (1) drop the result and return early,
		// or (2) wait until it times out to exit. For now we choose (2),
		// to avoid leaking resources in a non-obvious way. the only case
		// this affects is when the daemon is being started up and _immediately_
		// asked to close. other services are also starting up, so ok to wait.
		discoverdone := make(chan struct{})
		var nat *inat.NAT
		go func() {
			defer close(discoverdone)
			nat = inat.DiscoverNAT()
		}()

		// by this point -- after finding the NAT -- we may have already
		// be closing. if so, just exit.
		select {
		case <-worker.Closing():
			return
		case <-discoverdone:
			if nat == nil { // no nat, or failed to get it.
				return
			}
		}

		// wire up the nat to close when nmgr closes.
		// nmgr.proc is our parent, and waiting for us.
		nmgr.proc.AddChild(nat.Process())

		// set the nat.
		nmgr.natmu.Lock()
		nmgr.nat = nat
		nmgr.natmu.Unlock()

		// signal that we're ready to process nat mappings:
		close(nmgr.ready)

		// sign natManager up for network notifications
		// we need to sign up here to avoid missing some notifs
		// before the NAT has been found.
		nmgr.net.Notify((*nmgrNetNotifiee)(nmgr))

		// if any interfaces were brought up while we were setting up
		// the nat, now is the time to setup port mappings for them.
		// we release ready, then grab them to avoid losing any. adding
		// a port mapping is idempotent, so its ok to add the same twice.
		addrs := nmgr.net.ListenAddresses()
		for _, addr := range addrs {
			// we do it async because it's slow and we may want to close beforehand
			go addPortMapping(nmgr, addr)
		}
	})
}

// NAT returns the natManager's nat object. this may be nil, if
// (a) the search process is still ongoing, or (b) the search process
// found no nat. Clients must check whether the return value is nil.
func (nmgr *natManager) NAT() *inat.NAT {
	nmgr.natmu.Lock()
	defer nmgr.natmu.Unlock()
	return nmgr.nat
}

func addPortMapping(nmgr *natManager, intaddr ma.Multiaddr) {
	nat := nmgr.NAT()
	if nat == nil {
		panic("natManager addPortMapping called without a nat.")
	}

	// first, check if the port mapping already exists.
	for _, mapping := range nat.Mappings() {
		if mapping.InternalAddr().Equal(intaddr) {
			return // it exists! return.
		}
	}

	ctx := context.TODO()
	lm := make(lgbl.DeferredMap)
	lm["internalAddr"] = func() interface{} { return intaddr.String() }

	defer log.EventBegin(ctx, "natMgrAddPortMappingWait", lm).Done()

	select {
	case <-nmgr.proc.Closing():
		lm["outcome"] = "cancelled"
		return // no use.
	case <-nmgr.ready: // wait until it's ready.
	}

	// actually start the port map (sub-event because waiting may take a while)
	defer log.EventBegin(ctx, "natMgrAddPortMapping", lm).Done()

	// get the nat
	m, err := nat.NewMapping(intaddr)
	if err != nil {
		lm["outcome"] = "failure"
		lm["error"] = err
		return
	}

	extaddr, err := m.ExternalAddr()
	if err != nil {
		lm["outcome"] = "failure"
		lm["error"] = err
		return
	}

	// TODO: make these temporary and rediscover them.
	nmgr.net.Peerstore().AddAddr(nmgr.net.LocalPeer(), extaddr, pstore.PermanentAddrTTL)

	lm["outcome"] = "success"
	lm["externalAddr"] = func() interface{} { return extaddr.String() }
	log.Infof("established nat port mapping: %s <--> %s", intaddr, extaddr)
}

func rmPortMapping(nmgr *natManager, intaddr ma.Multiaddr) {
	nat := nmgr.NAT()
	if nat == nil {
		panic("natManager rmPortMapping called without a nat.")
	}

	// list the port mappings (it may be gone on it's own, so we need to
	// check this list, and not store it ourselves behind the scenes)

	// close mappings for this internal address.
	for _, mapping := range nat.Mappings() {
		if mapping.InternalAddr().Equal(intaddr) {
			mapping.Close()
		}
	}
}

// nmgrNetNotifiee implements the network notification listening part
// of the natManager. this is merely listening to Listen() and ListenClose()
// events.
type nmgrNetNotifiee natManager

func (nn *nmgrNetNotifiee) natManager() *natManager {
	return (*natManager)(nn)
}

func (nn *nmgrNetNotifiee) Listen(n inet.Network, addr ma.Multiaddr) {
	if nn.natManager().NAT() == nil {
		return // not ready or doesnt exist.
	}

	addPortMapping(nn.natManager(), addr)
}

func (nn *nmgrNetNotifiee) ListenClose(n inet.Network, addr ma.Multiaddr) {
	if nn.natManager().NAT() == nil {
		return // not ready or doesnt exist.
	}

	rmPortMapping(nn.natManager(), addr)
}

func (nn *nmgrNetNotifiee) Connected(inet.Network, inet.Conn)      {}
func (nn *nmgrNetNotifiee) Disconnected(inet.Network, inet.Conn)   {}
func (nn *nmgrNetNotifiee) OpenedStream(inet.Network, inet.Stream) {}
func (nn *nmgrNetNotifiee) ClosedStream(inet.Network, inet.Stream) {}
<commit_msg>Revert "Merge pull request #386 from libp2p/fix/correct-external-addr"<commit_after>package basichost

import (
	"context"
	"sync"

	goprocess "github.com/jbenet/goprocess"
	lgbl "github.com/libp2p/go-libp2p-loggables"
	inat "github.com/libp2p/go-libp2p-nat"
	inet "github.com/libp2p/go-libp2p-net"
	ma "github.com/multiformats/go-multiaddr"
)

// A simple interface to manage NAT devices.
type NATManager interface {

	// Get the NAT device managed by the NAT manager.
	NAT() *inat.NAT

	// Receive a notification when the NAT device is ready for use.
	Ready() <-chan struct{}

	// Close all resources associated with a NAT manager.
	Close() error
}

// Create a NAT manager.
func NewNATManager(net inet.Network) NATManager {
	return newNatManager(net)
}

// natManager takes care of adding + removing port mappings to the nat.
// Initialized with the host if it has a NATPortMap option enabled.
// natManager receives signals from the network, and check on nat mappings:
//  * natManager listens to the network and adds or closes port mappings
//    as the network signals Listen() or ListenClose().
//  * closing the natManager closes the nat and its mappings.
type natManager struct {
	net   inet.Network
	natmu sync.RWMutex // guards nat (ready could obviate this mutex, but safety first.)
	nat   *inat.NAT

	ready chan struct{}     // closed once the nat is ready to process port mappings
	proc  goprocess.Process // natManager has a process + children. can be closed.
}

func newNatManager(net inet.Network) *natManager {
	nmgr := &natManager{
		net:   net,
		ready: make(chan struct{}),
	}

	nmgr.proc = goprocess.WithTeardown(func() error {
		// on closing, unregister from network notifications.
		net.StopNotify((*nmgrNetNotifiee)(nmgr))
		return nil
	})

	// discover the nat.
	nmgr.discoverNAT()
	return nmgr
}

// Close closes the natManager, closing the underlying nat
// and unregistering from network events.
func (nmgr *natManager) Close() error {
	return nmgr.proc.Close()
}

// Ready returns a channel which will be closed when the NAT has been found
// and is ready to be used, or the search process is done.
func (nmgr *natManager) Ready() <-chan struct{} {
	return nmgr.ready
}

func (nmgr *natManager) discoverNAT() {

	nmgr.proc.Go(func(worker goprocess.Process) {
		// inat.DiscoverNAT blocks until the nat is found or a timeout
		// is reached. we unfortunately cannot specify timeouts-- the
		// library we're using just blocks.
		//
		// Note: on early shutdown, there may be a case where we're trying
		// to close before DiscoverNAT() returns. Since we cant cancel it
		// (library) we can choose to (1) drop the result and return early,
		// or (2) wait until it times out to exit. For now we choose (2),
		// to avoid leaking resources in a non-obvious way. the only case
		// this affects is when the daemon is being started up and _immediately_
		// asked to close. other services are also starting up, so ok to wait.
		discoverdone := make(chan struct{})
		var nat *inat.NAT
		go func() {
			defer close(discoverdone)
			nat = inat.DiscoverNAT()
		}()

		// by this point -- after finding the NAT -- we may have already
		// be closing. if so, just exit.
		select {
		case <-worker.Closing():
			return
		case <-discoverdone:
			if nat == nil { // no nat, or failed to get it.
				return
			}
		}

		// wire up the nat to close when nmgr closes.
		// nmgr.proc is our parent, and waiting for us.
		nmgr.proc.AddChild(nat.Process())

		// set the nat.
		nmgr.natmu.Lock()
		nmgr.nat = nat
		nmgr.natmu.Unlock()

		// signal that we're ready to process nat mappings:
		close(nmgr.ready)

		// sign natManager up for network notifications
		// we need to sign up here to avoid missing some notifs
		// before the NAT has been found.
		nmgr.net.Notify((*nmgrNetNotifiee)(nmgr))

		// if any interfaces were brought up while we were setting up
		// the nat, now is the time to setup port mappings for them.
		// we release ready, then grab them to avoid losing any. adding
		// a port mapping is idempotent, so its ok to add the same twice.
		addrs := nmgr.net.ListenAddresses()
		for _, addr := range addrs {
			// we do it async because it's slow and we may want to close beforehand
			go addPortMapping(nmgr, addr)
		}
	})
}

// NAT returns the natManager's nat object. this may be nil, if
// (a) the search process is still ongoing, or (b) the search process
// found no nat. Clients must check whether the return value is nil.
func (nmgr *natManager) NAT() *inat.NAT {
	nmgr.natmu.Lock()
	defer nmgr.natmu.Unlock()
	return nmgr.nat
}

func addPortMapping(nmgr *natManager, intaddr ma.Multiaddr) {
	nat := nmgr.NAT()
	if nat == nil {
		panic("natManager addPortMapping called without a nat.")
	}

	// first, check if the port mapping already exists.
	for _, mapping := range nat.Mappings() {
		if mapping.InternalAddr().Equal(intaddr) {
			return // it exists! return.
		}
	}

	ctx := context.TODO()
	lm := make(lgbl.DeferredMap)
	lm["internalAddr"] = func() interface{} { return intaddr.String() }

	defer log.EventBegin(ctx, "natMgrAddPortMappingWait", lm).Done()

	select {
	case <-nmgr.proc.Closing():
		lm["outcome"] = "cancelled"
		return // no use.
	case <-nmgr.ready: // wait until it's ready.
	}

	// actually start the port map (sub-event because waiting may take a while)
	defer log.EventBegin(ctx, "natMgrAddPortMapping", lm).Done()

	// get the nat
	m, err := nat.NewMapping(intaddr)
	if err != nil {
		lm["outcome"] = "failure"
		lm["error"] = err
		return
	}

	extaddr, err := m.ExternalAddr()
	if err != nil {
		lm["outcome"] = "failure"
		lm["error"] = err
		return
	}

	lm["outcome"] = "success"
	lm["externalAddr"] = func() interface{} { return extaddr.String() }
	log.Infof("established nat port mapping: %s <--> %s", intaddr, extaddr)
}

func rmPortMapping(nmgr *natManager, intaddr ma.Multiaddr) {
	nat := nmgr.NAT()
	if nat == nil {
		panic("natManager rmPortMapping called without a nat.")
	}

	// list the port mappings (it may be gone on it's own, so we need to
	// check this list, and not store it ourselves behind the scenes)

	// close mappings for this internal address.
	for _, mapping := range nat.Mappings() {
		if mapping.InternalAddr().Equal(intaddr) {
			mapping.Close()
		}
	}
}

// nmgrNetNotifiee implements the network notification listening part
// of the natManager. this is merely listening to Listen() and ListenClose()
// events.
type nmgrNetNotifiee natManager

func (nn *nmgrNetNotifiee) natManager() *natManager {
	return (*natManager)(nn)
}

func (nn *nmgrNetNotifiee) Listen(n inet.Network, addr ma.Multiaddr) {
	if nn.natManager().NAT() == nil {
		return // not ready or doesnt exist.
	}

	addPortMapping(nn.natManager(), addr)
}

func (nn *nmgrNetNotifiee) ListenClose(n inet.Network, addr ma.Multiaddr) {
	if nn.natManager().NAT() == nil {
		return // not ready or doesnt exist.
	}

	rmPortMapping(nn.natManager(), addr)
}

func (nn *nmgrNetNotifiee) Connected(inet.Network, inet.Conn)      {}
func (nn *nmgrNetNotifiee) Disconnected(inet.Network, inet.Conn)   {}
func (nn *nmgrNetNotifiee) OpenedStream(inet.Network, inet.Stream) {}
func (nn *nmgrNetNotifiee) ClosedStream(inet.Network, inet.Stream) {}
<|endoftext|>


<commit_before>/*
 * Minio Client (C) 2016 Minio, Inc.
 *
 * 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 (
	"os"
	"strconv"

	"github.com/minio/mc/pkg/console"
	"github.com/minio/minio/pkg/probe"
	"github.com/minio/minio/pkg/quick"
)

// Migrates session header version '6' to '7'. Only change is
// LastRemoved field which was added in version '7'.
func migrateSessionV6ToV7() {
	for _, sid := range getSessionIDs() {
		sV6Header, err := loadSessionV6Header(sid)
		fatalIf(err.Trace(sid), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")
		if sV6Header.Version == "7" { // It is new format.
			continue
		}

		sessionFile, err := getSessionFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session file.")

		// Initialize v7 header and migrate to new config.
		sV7Header := &sessionV7Header{}
		sV7Header.Version = "7"
		sV7Header.When = sV6Header.When
		sV7Header.RootPath = sV6Header.RootPath
		sV7Header.GlobalBoolFlags = sV6Header.GlobalBoolFlags
		sV7Header.GlobalIntFlags = sV6Header.GlobalIntFlags
		sV7Header.GlobalStringFlags = sV6Header.GlobalStringFlags
		sV7Header.CommandType = sV6Header.CommandType
		sV7Header.CommandArgs = sV6Header.CommandArgs
		sV7Header.CommandBoolFlags = sV6Header.CommandBoolFlags
		sV7Header.CommandIntFlags = sV6Header.CommandIntFlags
		sV7Header.CommandStringFlags = sV6Header.CommandStringFlags
		sV7Header.LastCopied = sV6Header.LastCopied
		sV7Header.LastRemoved = ""
		sV7Header.TotalBytes = sV6Header.TotalBytes
		sV7Header.TotalObjects = sV6Header.TotalObjects

		qs, err := quick.New(sV7Header)
		fatalIf(err.Trace(sid), "Unable to initialize quick config for session '7' header.")

		err = qs.Save(sessionFile)
		fatalIf(err.Trace(sid, sessionFile), "Unable to migrate session from '6' to '7'.")

		console.Println("Successfully migrated ‘" + sessionFile + "’ from version ‘" + sV6Header.Version + "’ to " + "‘" + sV7Header.Version + "’.")
	}
}

// Migrate session version '5' to version '6', all older sessions are
// in-fact removed and not migrated. All session files from '6' and
// above should be migrated - See: migrateSessionV6ToV7().
func migrateSessionV5ToV6() {
	for _, sid := range getSessionIDs() {
		sV6Header, err := loadSessionV6Header(sid)
		fatalIf(err.Trace(sid), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")

		sessionVersion, e := strconv.Atoi(sV6Header.Version)
		fatalIf(probe.NewError(e), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")

		if sessionVersion > 5 { // It is new format.
			continue
		}

		/*** Remove all session files older than v6 ***/

		sessionFile, err := getSessionFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session file.")

		sessionDataFile, err := getSessionDataFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session data file.")

		console.Println("Removing unsupported session file ‘" + sessionFile + "’ version ‘" + sV6Header.Version + "’.")
		if e := os.Remove(sessionFile); e != nil {
			fatalIf(probe.NewError(e), "Unable to remove version ‘"+sV6Header.Version+"’ session file ‘"+sessionFile+"’.")
		}
		if e := os.Remove(sessionDataFile); e != nil {
			fatalIf(probe.NewError(e), "Unable to remove version ‘"+sV6Header.Version+"’ session data file ‘"+sessionDataFile+"’.")
		}
	}
}
<commit_msg>Avoid an error when migrating a session file which is created but then removed by another mc instance (#1768)<commit_after>/*
 * Minio Client (C) 2016 Minio, Inc.
 *
 * 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 (
	"os"
	"strconv"

	"github.com/minio/mc/pkg/console"
	"github.com/minio/minio/pkg/probe"
	"github.com/minio/minio/pkg/quick"
)

// Migrates session header version '6' to '7'. Only change is
// LastRemoved field which was added in version '7'.
func migrateSessionV6ToV7() {
	for _, sid := range getSessionIDs() {
		sV6Header, err := loadSessionV6Header(sid)
		if err != nil && !os.IsNotExist(err.ToGoError()) {
			fatalIf(err.Trace(sid), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")
		}
		if sV6Header.Version == "7" { // It is new format.
			continue
		}

		sessionFile, err := getSessionFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session file.")

		// Initialize v7 header and migrate to new config.
		sV7Header := &sessionV7Header{}
		sV7Header.Version = "7"
		sV7Header.When = sV6Header.When
		sV7Header.RootPath = sV6Header.RootPath
		sV7Header.GlobalBoolFlags = sV6Header.GlobalBoolFlags
		sV7Header.GlobalIntFlags = sV6Header.GlobalIntFlags
		sV7Header.GlobalStringFlags = sV6Header.GlobalStringFlags
		sV7Header.CommandType = sV6Header.CommandType
		sV7Header.CommandArgs = sV6Header.CommandArgs
		sV7Header.CommandBoolFlags = sV6Header.CommandBoolFlags
		sV7Header.CommandIntFlags = sV6Header.CommandIntFlags
		sV7Header.CommandStringFlags = sV6Header.CommandStringFlags
		sV7Header.LastCopied = sV6Header.LastCopied
		sV7Header.LastRemoved = ""
		sV7Header.TotalBytes = sV6Header.TotalBytes
		sV7Header.TotalObjects = sV6Header.TotalObjects

		qs, err := quick.New(sV7Header)
		fatalIf(err.Trace(sid), "Unable to initialize quick config for session '7' header.")

		err = qs.Save(sessionFile)
		fatalIf(err.Trace(sid, sessionFile), "Unable to migrate session from '6' to '7'.")

		console.Println("Successfully migrated ‘" + sessionFile + "’ from version ‘" + sV6Header.Version + "’ to " + "‘" + sV7Header.Version + "’.")
	}
}

// Migrate session version '5' to version '6', all older sessions are
// in-fact removed and not migrated. All session files from '6' and
// above should be migrated - See: migrateSessionV6ToV7().
func migrateSessionV5ToV6() {
	for _, sid := range getSessionIDs() {
		sV6Header, err := loadSessionV6Header(sid)
		if err != nil && !os.IsNotExist(err.ToGoError()) {
			fatalIf(err.Trace(sid), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")
		}

		sessionVersion, e := strconv.Atoi(sV6Header.Version)
		fatalIf(probe.NewError(e), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")

		if sessionVersion > 5 { // It is new format.
			continue
		}

		/*** Remove all session files older than v6 ***/

		sessionFile, err := getSessionFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session file.")

		sessionDataFile, err := getSessionDataFile(sid)
		fatalIf(err.Trace(sid), "Unable to get session data file.")

		console.Println("Removing unsupported session file ‘" + sessionFile + "’ version ‘" + sV6Header.Version + "’.")
		if e := os.Remove(sessionFile); e != nil {
			fatalIf(probe.NewError(e), "Unable to remove version ‘"+sV6Header.Version+"’ session file ‘"+sessionFile+"’.")
		}
		if e := os.Remove(sessionDataFile); e != nil {
			fatalIf(probe.NewError(e), "Unable to remove version ‘"+sV6Header.Version+"’ session data file ‘"+sessionDataFile+"’.")
		}
	}
}
<|endoftext|>


<commit_before>package session

import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"github.com/Cristofori/kmud/combat"
	"github.com/Cristofori/kmud/events"
	"github.com/Cristofori/kmud/model"
	"github.com/Cristofori/kmud/types"
	"github.com/Cristofori/kmud/utils"
	// "log"
	// "os"
	"strings"
	"time"
)

type Session struct {
	conn io.ReadWriter
	user types.User
	pc   types.PC

	prompt string
	states map[string]string

	userInputChannel chan string
	inputModeChannel chan userInputMode
	prompterChannel  chan utils.Prompter
	panicChannel     chan interface{}
	eventChannel     chan events.Event

	silentMode bool
	replyId    types.Id
	lastInput  string

	// logger *log.Logger
}

func NewSession(conn io.ReadWriter, user types.User, pc types.PC) *Session {
	var session Session
	session.conn = conn
	session.user = user
	session.pc = pc

	session.prompt = "%h/%H> "
	session.states = map[string]string{}

	session.userInputChannel = make(chan string)
	session.inputModeChannel = make(chan userInputMode)
	session.prompterChannel = make(chan utils.Prompter)
	session.panicChannel = make(chan interface{})
	session.eventChannel = events.Register(pc)

	session.silentMode = false

	// file, err := os.OpenFile(pc.GetName()+".log", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
	// utils.PanicIfError(err)

	// session.logger = log.New(file, pc.GetName()+" ", log.LstdFlags)

	model.Login(pc)

	return &session
}

type userInputMode int

const (
	CleanUserInput userInputMode = iota
	RawUserInput   userInputMode = iota
)

func (self *Session) Exec() {
	defer events.Unregister(self.pc)
	defer model.Logout(self.pc)

	self.WriteLine("Welcome, " + self.pc.GetName())
	self.PrintRoom()

	// Main routine in charge of actually reading input from the connection object,
	// also has built in throttling to limit how fast we are allowed to process
	// commands from the user.
	go func() {
		defer func() {
			self.panicChannel <- recover()
		}()

		throttler := utils.NewThrottler(200 * time.Millisecond)

		for {
			mode := <-self.inputModeChannel
			prompter := <-self.prompterChannel
			input := ""

			switch mode {
			case CleanUserInput:
				input = utils.GetUserInputP(self.conn, prompter, self.user.GetColorMode())
			case RawUserInput:
				input = utils.GetRawUserInputP(self.conn, prompter, self.user.GetColorMode())
			default:
				panic("Unhandled case in switch statement (userInputMode)")
			}

			throttler.Sync()
			self.userInputChannel <- input
		}
	}()

	// Main loop
	for {
		input := self.getUserInputP(RawUserInput, self)
		if input == "" || input == "logout" || input == "quit" {
			return
		}

		if input == "." {
			input = self.lastInput
		}

		self.lastInput = input

		if strings.HasPrefix(input, "/") {
			self.handleCommand(utils.Argify(input[1:]))
		} else {
			self.handleAction(utils.Argify(input))
		}
	}
}

func (self *Session) WriteLinef(line string, a ...interface{}) {
	self.WriteLineColor(types.ColorWhite, line, a...)
}

func (self *Session) WriteLine(line string, a ...interface{}) {
	self.WriteLineColor(types.ColorWhite, line, a...)
}

func (self *Session) WriteLineColor(color types.Color, line string, a ...interface{}) {
	self.printLine(types.Colorize(color, fmt.Sprintf(line, a...)))
}

func (self *Session) printLine(line string, a ...interface{}) {
	self.Write(fmt.Sprintf(line+"\r\n", a...))
}

func (self *Session) Write(text string) {
	self.user.Write(text)
}

func (self *Session) printError(err string, a ...interface{}) {
	self.WriteLineColor(types.ColorRed, err, a...)
}

func (self *Session) clearLine() {
	utils.ClearLine(self.conn)
}

func (self *Session) asyncMessage(message string) {
	self.clearLine()
	self.WriteLine(message)
}

func (self *Session) GetInput(prompt string) string {
	return self.getUserInput(CleanUserInput, prompt)
}

func (self *Session) GetWindowSize() (int, int) {
	return self.user.GetWindowSize()
}

// getUserInput allows us to retrieve user input in a way that doesn't block the
// event loop by using channels and a separate Go routine to grab
// either the next user input or the next event.
func (self *Session) getUserInputP(inputMode userInputMode, prompter utils.Prompter) string {
	self.inputModeChannel <- inputMode
	self.prompterChannel <- prompter

	for {
		select {
		case input := <-self.userInputChannel:
			return input
		case event := <-self.eventChannel:
			if self.silentMode {
				continue
			}

			switch e := event.(type) {
			case events.TellEvent:
				self.replyId = e.From.GetId()
			case events.TickEvent:
				if !combat.InCombat(self.pc) {
					oldHps := self.pc.GetHitPoints()
					self.pc.Heal(5)
					newHps := self.pc.GetHitPoints()

					if oldHps != newHps {
						self.clearLine()
						self.Write(prompter.GetPrompt())
					}
				}
			}

			message := event.ToString(self.pc)
			if message != "" {
				self.asyncMessage(message)
				self.Write(prompter.GetPrompt())
			}

		case quitMessage := <-self.panicChannel:
			panic(quitMessage)
		}
	}
}

func (self *Session) getUserInput(inputMode userInputMode, prompt string) string {
	return self.getUserInputP(inputMode, utils.SimplePrompter(prompt))
}

func (self *Session) getCleanUserInput(prompt string) string {
	return self.getUserInput(CleanUserInput, prompt)
}

func (self *Session) getRawUserInput(prompt string) string {
	return self.getUserInput(RawUserInput, prompt)
}

func (self *Session) getConfirmation(prompt string) bool {
	answer := self.getCleanUserInput(types.Colorize(types.ColorWhite, prompt))
	return answer == "y" || answer == "yes"
}

func (self *Session) getInt(prompt string, min, max int) (int, bool) {
	for {
		input := self.getRawUserInput(prompt)
		if input == "" {
			return 0, false
		}

		val, err := utils.Atoir(input, min, max)

		if err != nil {
			self.printError(err.Error())
		} else {
			return val, true
		}
	}
}

func (self *Session) GetPrompt() string {
	prompt := self.prompt
	prompt = strings.Replace(prompt, "%h", strconv.Itoa(self.pc.GetHitPoints()), -1)
	prompt = strings.Replace(prompt, "%H", strconv.Itoa(self.pc.GetHealth()), -1)

	if len(self.states) > 0 {
		states := make([]string, len(self.states))

		i := 0
		for key, value := range self.states {
			states[i] = fmt.Sprintf("%s:%s", key, value)
			i++
		}

		prompt = fmt.Sprintf("%s %s", states, prompt)
	}

	return types.Colorize(types.ColorWhite, prompt)
}

func (self *Session) currentZone() types.Zone {
	return model.GetZone(self.GetRoom().GetZoneId())
}

func (self *Session) handleAction(action string, arg string) {
	if arg == "" {
		direction := types.StringToDirection(action)

		if direction != types.DirectionNone {
			if self.GetRoom().HasExit(direction) {
				err := model.MoveCharacter(self.pc, direction)
				if err == nil {
					self.PrintRoom()
				} else {
					self.printError(err.Error())
				}

			} else {
				self.printError("You can't go that way")
			}

			return
		}
	}

	handler, found := actions[action]

	if found {
		if handler.alias != "" {
			handler = actions[handler.alias]
		}
		handler.exec(self, arg)
	} else {
		self.printError("You can't do that")
	}
}

func (self *Session) handleCommand(name string, arg string) {
	if len(name) == 0 {
		return
	}

	if name[0] == '/' && self.user.IsAdmin() {
		quickRoom(self, name[1:])
		return
	}

	command, found := commands[name]

	if found {
		if command.alias != "" {
			command = commands[command.alias]
		}

		if command.admin && !self.user.IsAdmin() {
			self.printError("You don't have permission to do that")
		} else {
			command.exec(command, self, arg)
		}
	} else {
		self.printError("Unrecognized command: %s", name)
	}
}

func (self *Session) GetRoom() types.Room {
	return model.GetRoom(self.pc.GetRoomId())
}

func (self *Session) PrintRoom() {
	self.printRoom(self.GetRoom())
}

func (self *Session) printRoom(room types.Room) {
	pcs := model.PlayerCharactersIn(self.pc.GetRoomId(), self.pc.GetId())
	npcs := model.NpcsIn(room.GetId())
	items := model.ItemsIn(room.GetId())
	store := model.StoreIn(room.GetId())

	var area types.Area
	if room.GetAreaId() != nil {
		area = model.GetArea(room.GetAreaId())
	}

	var str string

	areaStr := ""
	if area != nil {
		areaStr = fmt.Sprintf("%s - ", area.GetName())
	}

	str = fmt.Sprintf("\r\n %v>>> %v%s%s %v<<< %v(%v %v %v)\r\n\r\n %v%s\r\n\r\n",
		types.ColorWhite, types.ColorBlue,
		areaStr, room.GetTitle(),
		types.ColorWhite, types.ColorBlue,
		room.GetLocation().X, room.GetLocation().Y, room.GetLocation().Z,
		types.ColorWhite,
		room.GetDescription())

	if store != nil {
		str = fmt.Sprintf("%s Store: %s\r\n\r\n", str, types.Colorize(types.ColorBlue, store.GetName()))
	}

	extraNewLine := ""

	if len(pcs) > 0 {
		str = fmt.Sprintf("%s %sAlso here:", str, types.ColorBlue)

		names := make([]string, len(pcs))
		for i, char := range pcs {
			names[i] = types.Colorize(types.ColorWhite, char.GetName())
		}
		str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", ")))

		extraNewLine = "\r\n"
	}

	if len(npcs) > 0 {
		str = fmt.Sprintf("%s %s", str, types.Colorize(types.ColorBlue, "NPCs: "))

		names := make([]string, len(npcs))
		for i, npc := range npcs {
			names[i] = types.Colorize(types.ColorWhite, npc.GetName())
		}
		str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", ")))

		extraNewLine = "\r\n"
	}

	if len(items) > 0 {
		itemMap := make(map[string]int)
		var nameList []string

		for _, item := range items {
			if item == nil {
				continue
			}

			_, found := itemMap[item.GetName()]
			if !found {
				nameList = append(nameList, item.GetName())
			}
			itemMap[item.GetName()]++
		}

		sort.Strings(nameList)

		str = str + " " + types.Colorize(types.ColorBlue, "Items: ")

		var names []string
		for _, name := range nameList {
			if itemMap[name] > 1 {
				name = fmt.Sprintf("%s x%v", name, itemMap[name])
			}
			names = append(names, types.Colorize(types.ColorWhite, name))
		}
		str = str + strings.Join(names, types.Colorize(types.ColorBlue, ", ")) + "\r\n"

		extraNewLine = "\r\n"
	}

	str = str + extraNewLine + " " + types.Colorize(types.ColorBlue, "Exits: ")

	var exitList []string
	for _, direction := range room.GetExits() {
		exitList = append(exitList, utils.DirectionToExitString(direction))
	}

	if len(exitList) == 0 {
		str = str + types.Colorize(types.ColorWhite, "None")
	} else {
		str = str + strings.Join(exitList, " ")
	}

	if len(room.GetLinks()) > 0 {
		str = fmt.Sprintf("%s\r\n\r\n %s %s",
			str,
			types.Colorize(types.ColorBlue, "Other exits:"),
			types.Colorize(types.ColorWhite, strings.Join(room.LinkNames(), ", ")),
		)
	}

	str = str + "\r\n"

	self.WriteLine(str)
}
<commit_msg>Echo the last command back to the user when using the . command<commit_after>package session

import (
	"fmt"
	"io"
	"sort"
	"strconv"

	"github.com/Cristofori/kmud/combat"
	"github.com/Cristofori/kmud/events"
	"github.com/Cristofori/kmud/model"
	"github.com/Cristofori/kmud/types"
	"github.com/Cristofori/kmud/utils"
	// "log"
	// "os"
	"strings"
	"time"
)

type Session struct {
	conn io.ReadWriter
	user types.User
	pc   types.PC

	prompt string
	states map[string]string

	userInputChannel chan string
	inputModeChannel chan userInputMode
	prompterChannel  chan utils.Prompter
	panicChannel     chan interface{}
	eventChannel     chan events.Event

	silentMode bool
	replyId    types.Id
	lastInput  string

	// logger *log.Logger
}

func NewSession(conn io.ReadWriter, user types.User, pc types.PC) *Session {
	var session Session
	session.conn = conn
	session.user = user
	session.pc = pc

	session.prompt = "%h/%H> "
	session.states = map[string]string{}

	session.userInputChannel = make(chan string)
	session.inputModeChannel = make(chan userInputMode)
	session.prompterChannel = make(chan utils.Prompter)
	session.panicChannel = make(chan interface{})
	session.eventChannel = events.Register(pc)

	session.silentMode = false

	// file, err := os.OpenFile(pc.GetName()+".log", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm)
	// utils.PanicIfError(err)

	// session.logger = log.New(file, pc.GetName()+" ", log.LstdFlags)

	model.Login(pc)

	return &session
}

type userInputMode int

const (
	CleanUserInput userInputMode = iota
	RawUserInput   userInputMode = iota
)

func (self *Session) Exec() {
	defer events.Unregister(self.pc)
	defer model.Logout(self.pc)

	self.WriteLine("Welcome, " + self.pc.GetName())
	self.PrintRoom()

	// Main routine in charge of actually reading input from the connection object,
	// also has built in throttling to limit how fast we are allowed to process
	// commands from the user.
	go func() {
		defer func() {
			self.panicChannel <- recover()
		}()

		throttler := utils.NewThrottler(200 * time.Millisecond)

		for {
			mode := <-self.inputModeChannel
			prompter := <-self.prompterChannel
			input := ""

			switch mode {
			case CleanUserInput:
				input = utils.GetUserInputP(self.conn, prompter, self.user.GetColorMode())
			case RawUserInput:
				input = utils.GetRawUserInputP(self.conn, prompter, self.user.GetColorMode())
			default:
				panic("Unhandled case in switch statement (userInputMode)")
			}

			throttler.Sync()
			self.userInputChannel <- input
		}
	}()

	// Main loop
	for {
		input := self.getUserInputP(RawUserInput, self)
		if input == "" || input == "logout" || input == "quit" {
			return
		}

		if input == "." {
			self.WriteLine(self.lastInput)
			input = self.lastInput
		}

		self.lastInput = input

		if strings.HasPrefix(input, "/") {
			self.handleCommand(utils.Argify(input[1:]))
		} else {
			self.handleAction(utils.Argify(input))
		}
	}
}

func (self *Session) WriteLinef(line string, a ...interface{}) {
	self.WriteLineColor(types.ColorWhite, line, a...)
}

func (self *Session) WriteLine(line string, a ...interface{}) {
	self.WriteLineColor(types.ColorWhite, line, a...)
}

func (self *Session) WriteLineColor(color types.Color, line string, a ...interface{}) {
	self.printLine(types.Colorize(color, fmt.Sprintf(line, a...)))
}

func (self *Session) printLine(line string, a ...interface{}) {
	self.Write(fmt.Sprintf(line+"\r\n", a...))
}

func (self *Session) Write(text string) {
	self.user.Write(text)
}

func (self *Session) printError(err string, a ...interface{}) {
	self.WriteLineColor(types.ColorRed, err, a...)
}

func (self *Session) clearLine() {
	utils.ClearLine(self.conn)
}

func (self *Session) asyncMessage(message string) {
	self.clearLine()
	self.WriteLine(message)
}

func (self *Session) GetInput(prompt string) string {
	return self.getUserInput(CleanUserInput, prompt)
}

func (self *Session) GetWindowSize() (int, int) {
	return self.user.GetWindowSize()
}

// getUserInput allows us to retrieve user input in a way that doesn't block the
// event loop by using channels and a separate Go routine to grab
// either the next user input or the next event.
func (self *Session) getUserInputP(inputMode userInputMode, prompter utils.Prompter) string {
	self.inputModeChannel <- inputMode
	self.prompterChannel <- prompter

	for {
		select {
		case input := <-self.userInputChannel:
			return input
		case event := <-self.eventChannel:
			if self.silentMode {
				continue
			}

			switch e := event.(type) {
			case events.TellEvent:
				self.replyId = e.From.GetId()
			case events.TickEvent:
				if !combat.InCombat(self.pc) {
					oldHps := self.pc.GetHitPoints()
					self.pc.Heal(5)
					newHps := self.pc.GetHitPoints()

					if oldHps != newHps {
						self.clearLine()
						self.Write(prompter.GetPrompt())
					}
				}
			}

			message := event.ToString(self.pc)
			if message != "" {
				self.asyncMessage(message)
				self.Write(prompter.GetPrompt())
			}

		case quitMessage := <-self.panicChannel:
			panic(quitMessage)
		}
	}
}

func (self *Session) getUserInput(inputMode userInputMode, prompt string) string {
	return self.getUserInputP(inputMode, utils.SimplePrompter(prompt))
}

func (self *Session) getCleanUserInput(prompt string) string {
	return self.getUserInput(CleanUserInput, prompt)
}

func (self *Session) getRawUserInput(prompt string) string {
	return self.getUserInput(RawUserInput, prompt)
}

func (self *Session) getConfirmation(prompt string) bool {
	answer := self.getCleanUserInput(types.Colorize(types.ColorWhite, prompt))
	return answer == "y" || answer == "yes"
}

func (self *Session) getInt(prompt string, min, max int) (int, bool) {
	for {
		input := self.getRawUserInput(prompt)
		if input == "" {
			return 0, false
		}

		val, err := utils.Atoir(input, min, max)

		if err != nil {
			self.printError(err.Error())
		} else {
			return val, true
		}
	}
}

func (self *Session) GetPrompt() string {
	prompt := self.prompt
	prompt = strings.Replace(prompt, "%h", strconv.Itoa(self.pc.GetHitPoints()), -1)
	prompt = strings.Replace(prompt, "%H", strconv.Itoa(self.pc.GetHealth()), -1)

	if len(self.states) > 0 {
		states := make([]string, len(self.states))

		i := 0
		for key, value := range self.states {
			states[i] = fmt.Sprintf("%s:%s", key, value)
			i++
		}

		prompt = fmt.Sprintf("%s %s", states, prompt)
	}

	return types.Colorize(types.ColorWhite, prompt)
}

func (self *Session) currentZone() types.Zone {
	return model.GetZone(self.GetRoom().GetZoneId())
}

func (self *Session) handleAction(action string, arg string) {
	if arg == "" {
		direction := types.StringToDirection(action)

		if direction != types.DirectionNone {
			if self.GetRoom().HasExit(direction) {
				err := model.MoveCharacter(self.pc, direction)
				if err == nil {
					self.PrintRoom()
				} else {
					self.printError(err.Error())
				}

			} else {
				self.printError("You can't go that way")
			}

			return
		}
	}

	handler, found := actions[action]

	if found {
		if handler.alias != "" {
			handler = actions[handler.alias]
		}
		handler.exec(self, arg)
	} else {
		self.printError("You can't do that")
	}
}

func (self *Session) handleCommand(name string, arg string) {
	if len(name) == 0 {
		return
	}

	if name[0] == '/' && self.user.IsAdmin() {
		quickRoom(self, name[1:])
		return
	}

	command, found := commands[name]

	if found {
		if command.alias != "" {
			command = commands[command.alias]
		}

		if command.admin && !self.user.IsAdmin() {
			self.printError("You don't have permission to do that")
		} else {
			command.exec(command, self, arg)
		}
	} else {
		self.printError("Unrecognized command: %s", name)
	}
}

func (self *Session) GetRoom() types.Room {
	return model.GetRoom(self.pc.GetRoomId())
}

func (self *Session) PrintRoom() {
	self.printRoom(self.GetRoom())
}

func (self *Session) printRoom(room types.Room) {
	pcs := model.PlayerCharactersIn(self.pc.GetRoomId(), self.pc.GetId())
	npcs := model.NpcsIn(room.GetId())
	items := model.ItemsIn(room.GetId())
	store := model.StoreIn(room.GetId())

	var area types.Area
	if room.GetAreaId() != nil {
		area = model.GetArea(room.GetAreaId())
	}

	var str string

	areaStr := ""
	if area != nil {
		areaStr = fmt.Sprintf("%s - ", area.GetName())
	}

	str = fmt.Sprintf("\r\n %v>>> %v%s%s %v<<< %v(%v %v %v)\r\n\r\n %v%s\r\n\r\n",
		types.ColorWhite, types.ColorBlue,
		areaStr, room.GetTitle(),
		types.ColorWhite, types.ColorBlue,
		room.GetLocation().X, room.GetLocation().Y, room.GetLocation().Z,
		types.ColorWhite,
		room.GetDescription())

	if store != nil {
		str = fmt.Sprintf("%s Store: %s\r\n\r\n", str, types.Colorize(types.ColorBlue, store.GetName()))
	}

	extraNewLine := ""

	if len(pcs) > 0 {
		str = fmt.Sprintf("%s %sAlso here:", str, types.ColorBlue)

		names := make([]string, len(pcs))
		for i, char := range pcs {
			names[i] = types.Colorize(types.ColorWhite, char.GetName())
		}
		str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", ")))

		extraNewLine = "\r\n"
	}

	if len(npcs) > 0 {
		str = fmt.Sprintf("%s %s", str, types.Colorize(types.ColorBlue, "NPCs: "))

		names := make([]string, len(npcs))
		for i, npc := range npcs {
			names[i] = types.Colorize(types.ColorWhite, npc.GetName())
		}
		str = fmt.Sprintf("%s %s \r\n", str, strings.Join(names, types.Colorize(types.ColorBlue, ", ")))

		extraNewLine = "\r\n"
	}

	if len(items) > 0 {
		itemMap := make(map[string]int)
		var nameList []string

		for _, item := range items {
			if item == nil {
				continue
			}

			_, found := itemMap[item.GetName()]
			if !found {
				nameList = append(nameList, item.GetName())
			}
			itemMap[item.GetName()]++
		}

		sort.Strings(nameList)

		str = str + " " + types.Colorize(types.ColorBlue, "Items: ")

		var names []string
		for _, name := range nameList {
			if itemMap[name] > 1 {
				name = fmt.Sprintf("%s x%v", name, itemMap[name])
			}
			names = append(names, types.Colorize(types.ColorWhite, name))
		}
		str = str + strings.Join(names, types.Colorize(types.ColorBlue, ", ")) + "\r\n"

		extraNewLine = "\r\n"
	}

	str = str + extraNewLine + " " + types.Colorize(types.ColorBlue, "Exits: ")

	var exitList []string
	for _, direction := range room.GetExits() {
		exitList = append(exitList, utils.DirectionToExitString(direction))
	}

	if len(exitList) == 0 {
		str = str + types.Colorize(types.ColorWhite, "None")
	} else {
		str = str + strings.Join(exitList, " ")
	}

	if len(room.GetLinks()) > 0 {
		str = fmt.Sprintf("%s\r\n\r\n %s %s",
			str,
			types.Colorize(types.ColorBlue, "Other exits:"),
			types.Colorize(types.ColorWhite, strings.Join(room.LinkNames(), ", ")),
		)
	}

	str = str + "\r\n"

	self.WriteLine(str)
}
<|endoftext|>


<commit_before>package animecmd

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/jasonpuglisi/inami-irc-bot/configutil"
	"github.com/jasonpuglisi/ircutil"
)

// Init adds animecmd's functions to the command map.
func Init(cmdMap ircutil.CmdMap) {
	ircutil.AddCommand(cmdMap, "inami/animecmd.Countdown", Countdown)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Alias", Alias)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Search", Search)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Watch", Watch)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Progress", Progress)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Next", Next)
}

// Countdown starts a countdown to coordinate group watching.
// Function key: inami.animeutil/Countdown
func Countdown(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Send response for countdown start.
	ircutil.SendResponse(client, message.Source, message.Target,
		"Starting countdown, press play when I say \"Start!\"")

	// Send response with seconds remaining, or "Start!" at 0, and decrement
	// seconds remaining.
	ticker := time.NewTicker(time.Second)
	i := 6
	for range ticker.C {
		s := strconv.Itoa(i)
		if i == 0 {
			s = "Start!"
		}
		if i < 6 {
			ircutil.SendResponse(client, message.Source, message.Target,
				s)
		}
		if i == 0 {
			break
		}
		i--
	}
	ticker.Stop()
}

// Alias saves a show identifier as a custom alias.
func Alias(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and value, and update scope/owner to match command scope.
	id, alias := message.Args[0], message.Args[1]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Set alias, intialize episode progress, and send response with
	// confirmation.
	configutil.SetValue(client, keys, id)
	keys[2] = "anime/progress"
	configutil.SetValue(client, keys, "0")
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Aliased %s to %s", id, alias))
}

// Search searches an anime database and returns relevant search results.
// Function key: inami/animecmd.Search
func Search(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Search Kitsu for shows matching query.
	shows, err := search(strings.Join(message.Args, " "))
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching shows, try again later")
		return
	}

	// Send response if no shows are found.
	if len(shows) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"No shows found")
		return
	}

	// Send response with found shows and their URLs.
	ircutil.SendResponse(client, message.Source, message.Target,
		"Shows found:")
	for _, s := range shows {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("- [%s] %s", s.ID, s.Attributes.Title))
	}
}

// Watch gets data of a show and starts a group watching session.
// Function key: inami/animecmd.Watch
func Watch(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show id from alias in persistent data.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error getting alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Get episode number from alias in persistent data.
	keys[2] = "anime/progress"
	numStr, _ := configutil.GetValue(client, keys)
	num, _ := strconv.Atoi(numStr)
	num++

	// Get show data from Kitsu.
	show, err := show(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching show, try again later")
		return
	}

	// Send response if show not found.
	if len(show.Attributes.Slug) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("Show not found, make sure your alias is using %s",
				"the name after /anime/ in the show's URL"))
		return
	}

	// Get episode data from Kitsu.
	episodes, err := episodes(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching episodes, try again later")
		return
	}

	// Find specific episode.
	var episode *result
	for i := range episodes {
		e := &episodes[i]
		if e.Attributes.Number == num {
			episode = e
		}
	}

	// Clear episode title if it doesn't exist.
	episodeTitle := ""
	if episode != nil && len(episode.Attributes.Title) > 0 {
		episodeTitle = fmt.Sprintf(" \"%s\"", episode.Attributes.Title)
	}

	// Start countdown and sleep before sending episode title.
	Countdown(client, command, message)
	time.Sleep(time.Second * 5)

	// Send response with episode information, and increment episode number.
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("You're watching %s Episode %d%s", show.Attributes.Title, num,
			episodeTitle))
	configutil.SetValue(client, keys, strconv.Itoa(num))
}

// Progress updates a show's episode number.
func Progress(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Parse episode number from arguments.
	num, err := strconv.Atoi(message.Args[1])
	if err != nil || num < 0 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Invalid episode number")
	}

	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show name from alias in persistent data to make sure it exists.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error checking for alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Set episode number in persistent data and send response with confirmation.
	keys[2] = "anime/progress"
	plural := "s"
	if num == 1 {
		plural = ""
	}
	configutil.SetValue(client, keys, strconv.Itoa(num))
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Updated show progress, you've watched %d episode%s", num,
			plural))
}

// Next returns information about the next episode for a show.
func Next(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show id from alias in persistent data to make sure it exists.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error checking for alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Get episode number from alias in persistent data.
	keys[2] = "anime/progress"
	strNum, _ := configutil.GetValue(client, keys)
	num, _ := strconv.Atoi(strNum)
	num++

	// Get show data from Kitsu.
	show, err := show(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching show, try again later")
		return
	}

	// Send response if show not found.
	if len(show.Attributes.Slug) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("Show not found, make sure your alias is using %s",
				"the name after /anime/ in the show's URL"))
		return
	}

	// Get episode data from Kitsu.
	episodes, err := episodes(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching episodes, try again later")
		return
	}

	// Find specific episode.
	var episode *result
	for i := range episodes {
		e := &episodes[i]
		if e.Attributes.Number == num {
			episode = e
		}
	}

	// Clear episode title if it doesn't exist.
	episodeTitle := ""
	if episode != nil && len(episode.Attributes.Title) > 0 {
		episodeTitle = fmt.Sprintf(" \"%s\"", episode.Attributes.Title)
	}

	// Send response with next episode information.
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Next up for %s is Episode %d%s", show.Attributes.Title, num,
			episodeTitle))
}
<commit_msg>Break episode checking loop whenever possible<commit_after>package animecmd

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/jasonpuglisi/inami-irc-bot/configutil"
	"github.com/jasonpuglisi/ircutil"
)

// Init adds animecmd's functions to the command map.
func Init(cmdMap ircutil.CmdMap) {
	ircutil.AddCommand(cmdMap, "inami/animecmd.Countdown", Countdown)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Alias", Alias)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Search", Search)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Watch", Watch)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Progress", Progress)
	ircutil.AddCommand(cmdMap, "inami/animecmd.Next", Next)
}

// Countdown starts a countdown to coordinate group watching.
// Function key: inami.animeutil/Countdown
func Countdown(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Send response for countdown start.
	ircutil.SendResponse(client, message.Source, message.Target,
		"Starting countdown, press play when I say \"Start!\"")

	// Send response with seconds remaining, or "Start!" at 0, and decrement
	// seconds remaining.
	ticker := time.NewTicker(time.Second)
	i := 6
	for range ticker.C {
		s := strconv.Itoa(i)
		if i == 0 {
			s = "Start!"
		}
		if i < 6 {
			ircutil.SendResponse(client, message.Source, message.Target,
				s)
		}
		if i == 0 {
			break
		}
		i--
	}
	ticker.Stop()
}

// Alias saves a show identifier as a custom alias.
func Alias(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and value, and update scope/owner to match command scope.
	id, alias := message.Args[0], message.Args[1]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Set alias, intialize episode progress, and send response with
	// confirmation.
	configutil.SetValue(client, keys, id)
	keys[2] = "anime/progress"
	configutil.SetValue(client, keys, "0")
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Aliased %s to %s", id, alias))
}

// Search searches an anime database and returns relevant search results.
// Function key: inami/animecmd.Search
func Search(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Search Kitsu for shows matching query.
	shows, err := search(strings.Join(message.Args, " "))
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching shows, try again later")
		return
	}

	// Send response if no shows are found.
	if len(shows) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"No shows found")
		return
	}

	// Send response with found shows and their URLs.
	ircutil.SendResponse(client, message.Source, message.Target,
		"Shows found:")
	for _, s := range shows {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("- [%s] %s", s.ID, s.Attributes.Title))
	}
}

// Watch gets data of a show and starts a group watching session.
// Function key: inami/animecmd.Watch
func Watch(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show id from alias in persistent data.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error getting alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Get episode number from alias in persistent data.
	keys[2] = "anime/progress"
	numStr, _ := configutil.GetValue(client, keys)
	num, _ := strconv.Atoi(numStr)
	num++

	// Get show data from Kitsu.
	show, err := show(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching show, try again later")
		return
	}

	// Send response if show not found.
	if len(show.Attributes.Slug) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("Show not found, make sure your alias is using %s",
				"the name after /anime/ in the show's URL"))
		return
	}

	// Get episode data from Kitsu.
	episodes, err := episodes(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching episodes, try again later")
		return
	}

	// Find specific episode.
	var episode *result
	for i := range episodes {
		e := &episodes[i]
		if e.Attributes.Number == num {
			episode = e
      break
		}
	}

	// Clear episode title if it doesn't exist.
	episodeTitle := ""
	if episode != nil && len(episode.Attributes.Title) > 0 {
		episodeTitle = fmt.Sprintf(" \"%s\"", episode.Attributes.Title)
	}

	// Start countdown and sleep before sending episode title.
	Countdown(client, command, message)
	time.Sleep(time.Second * 5)

	// Send response with episode information, and increment episode number.
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("You're watching %s Episode %d%s", show.Attributes.Title, num,
			episodeTitle))
	configutil.SetValue(client, keys, strconv.Itoa(num))
}

// Progress updates a show's episode number.
func Progress(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Parse episode number from arguments.
	num, err := strconv.Atoi(message.Args[1])
	if err != nil || num < 0 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Invalid episode number")
	}

	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show name from alias in persistent data to make sure it exists.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error checking for alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Set episode number in persistent data and send response with confirmation.
	keys[2] = "anime/progress"
	plural := "s"
	if num == 1 {
		plural = ""
	}
	configutil.SetValue(client, keys, strconv.Itoa(num))
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Updated show progress, you've watched %d episode%s", num,
			plural))
}

// Next returns information about the next episode for a show.
func Next(client *ircutil.Client, command *ircutil.Command,
	message *ircutil.Message) {
	// Set alias and update scope/owner to match command scope.
	alias := message.Args[0]
	keys := []string{"", "", "anime/shows", alias}
	configutil.UpdateScope(keys, message.Source, message.Target)

	// Get show id from alias in persistent data to make sure it exists.
	id, err := configutil.GetValue(client, keys)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error checking for alias, try again later")
		return
	}
	if len(id) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			"Alias not found, make sure you've assigned a show to it")
		return
	}

	// Get episode number from alias in persistent data.
	keys[2] = "anime/progress"
	strNum, _ := configutil.GetValue(client, keys)
	num, _ := strconv.Atoi(strNum)
	num++

	// Get show data from Kitsu.
	show, err := show(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching show, try again later")
		return
	}

	// Send response if show not found.
	if len(show.Attributes.Slug) < 1 {
		ircutil.SendResponse(client, message.Source, message.Target,
			fmt.Sprintf("Show not found, make sure your alias is using %s",
				"the name after /anime/ in the show's URL"))
		return
	}

	// Get episode data from Kitsu.
	episodes, err := episodes(id)
	if err != nil {
		ircutil.Log(client, err.Error())
		ircutil.SendResponse(client, message.Source, message.Target,
			"Error fetching episodes, try again later")
		return
	}

	// Find specific episode.
	var episode *result
	for i := range episodes {
		e := &episodes[i]
		if e.Attributes.Number == num {
			episode = e
      break
		}
	}

	// Clear episode title if it doesn't exist.
	episodeTitle := ""
	if episode != nil && len(episode.Attributes.Title) > 0 {
		episodeTitle = fmt.Sprintf(" \"%s\"", episode.Attributes.Title)
	}

	// Send response with next episode information.
	ircutil.SendResponse(client, message.Source, message.Target,
		fmt.Sprintf("Next up for %s is Episode %d%s", show.Attributes.Title, num,
			episodeTitle))
}
<|endoftext|>


<commit_before>// +build linux

package keyring

import (
	"github.com/aulanov/go.dbus"
	"fmt"
	"os"
	"encoding/json"
)

const (
	DBUS_SERVICE_NAME = "org.kde.kwalletd"
	DBUS_PATH = "/modules/kwalletd"
	APPID = "aws-vault"
	FOLDER = "aws-vault"
)

func init() {
	wallet, err := newKwallet()
	if err != nil {
		fmt.Fprintln(os.Stderr, "Failed to connect to kwallet:", err)
		return
	}

	supportedBackends[KWalletBackend] = opener(func(name string) (Keyring, error) {
		if name == "" {
			name = "kdewallet"
		}

		return &kwalletKeyring{
			wallet: *wallet,
			name: name,
		}, nil
	})
}

type kwalletKeyring struct {
	wallet kwalletBinding
	name string
	handle int32
}

func (k *kwalletKeyring) openWallet() error {
	isOpen, err := k.wallet.IsOpen(k.handle)
	if err != nil {
		return err
	}

	if !isOpen {
		handle, err := k.wallet.Open(k.name, 0, APPID)
		if err != nil {
			return err
		}
		k.handle = handle
	}

	return nil
}

func (k *kwalletKeyring) Get(key string) (Item, error) {
	err := k.openWallet()
	if err != nil {
		return Item{}, err
	}

	data, err := k.wallet.ReadEntry(k.handle, FOLDER, key, APPID)
	if err != nil {
		return Item{}, err
	}

	item := Item{}
	err = json.Unmarshal(data, &item)
	if err != nil {
		return Item{}, err
	}

	return item, nil
}

func (k *kwalletKeyring) Set(item Item) error {
	err := k.openWallet()
	if err != nil {
		return err
	}

	data, err := json.Marshal(item)
	if err != nil {
		return err
	}

	err = k.wallet.WriteEntry(k.handle, FOLDER, item.Key, data, APPID)
	if err != nil {
		return err
	}

	return nil
}

func (k *kwalletKeyring) Remove(key string) error {
	err := k.openWallet()
	if err != nil {
		return err
	}

	err = k.wallet.RemoveEntry(k.handle, FOLDER, key, APPID)
	if err != nil {
		return err
	}

	return nil
}

func (k *kwalletKeyring) Keys() ([]string, error) {
	err := k.openWallet()
	if err != nil {
		return []string{}, err
	}

	entries, err := k.wallet.EntryList(k.handle, FOLDER, APPID)
	if err != nil {
		return []string{}, err
	}

	return entries, nil
}

func newKwallet() (*kwalletBinding, error) {
	conn, err := dbus.SessionBus()
	if err != nil {
		return nil, err
	}

	return &kwalletBinding{
		conn.Object(DBUS_SERVICE_NAME, DBUS_PATH),
	}, nil
}

// Dumb Dbus bindings for kwallet bindings with types
type kwalletBinding struct {
	dbus dbus.BusObject
}

// method bool org.kde.KWallet.isOpen(int handle)
func (k *kwalletBinding) IsOpen(handle int32) (bool, error) {
	call := k.dbus.Call("org.kde.KWallet.isOpen", 0, handle)
	if call.Err != nil {
		return false, call.Err
	}

	return call.Body[0].(bool), call.Err
}

// method int org.kde.KWallet.open(QString wallet, qlonglong wId, QString appid)
func (k *kwalletBinding) Open(name string, wID int64, appid string) (int32, error) {
	call := k.dbus.Call("org.kde.KWallet.open", 0, name, wID, appid)
	if call.Err != nil {
		return 0, call.Err
	}

	return call.Body[0].(int32), call.Err
}

// method QStringList org.kde.KWallet.entryList(int handle, QString folder, QString appid)
func (k *kwalletBinding) EntryList(handle int32, folder string, appid string) ([]string, error) {
	call := k.dbus.Call("org.kde.KWallet.entryList", 0, handle, folder, appid)
	if call.Err != nil {
		return []string{}, call.Err
	}

	return call.Body[0].([]string), call.Err
}

// method int org.kde.KWallet.writeEntry(int handle, QString folder, QString key, QByteArray value, QString appid)
func (k *kwalletBinding) WriteEntry(handle int32, folder string, key string, value []byte, appid string) error {
	call := k.dbus.Call("org.kde.KWallet.writeEntry", 0, handle, folder, key, value, appid)
	if call.Err != nil {
		return call.Err
	}

	return call.Err
}

// method int org.kde.KWallet.removeEntry(int handle, QString folder, QString key, QString appid)
func (k *kwalletBinding) RemoveEntry(handle int32, folder string, key string, appid string) error {
	call := k.dbus.Call("org.kde.KWallet.removeEntry", 0, handle, folder, key, appid)
	if call.Err != nil {
		return call.Err
	}

	return call.Err
}

// method QByteArray org.kde.KWallet.readEntry(int handle, QString folder, QString key, QString appid)
func (k *kwalletBinding) ReadEntry(handle int32, folder string, key string, appid string) ([]byte, error) {
	call := k.dbus.Call("org.kde.KWallet.readEntry", 0, handle, folder, key, appid)
	if call.Err != nil {
		return []byte{}, call.Err
	}

	return call.Body[0].([]byte), call.Err
}
<commit_msg>Only try and contact dbus if the backend is selected<commit_after>// +build linux

package keyring

import (
	"encoding/json"

	"github.com/aulanov/go.dbus"
)

const (
	DBUS_SERVICE_NAME = "org.kde.kwalletd"
	DBUS_PATH         = "/modules/kwalletd"
	APPID             = "aws-vault"
	FOLDER            = "aws-vault"
)

func init() {
	supportedBackends[KWalletBackend] = opener(func(name string) (Keyring, error) {
		if name == "" {
			name = "kdewallet"
		}

		wallet, err := newKwallet()
		if err != nil {
			return nil, err
		}

		return &kwalletKeyring{
			wallet: *wallet,
			name:   name,
		}, nil
	})
}

type kwalletKeyring struct {
	wallet kwalletBinding
	name   string
	handle int32
}

func (k *kwalletKeyring) openWallet() error {
	isOpen, err := k.wallet.IsOpen(k.handle)
	if err != nil {
		return err
	}

	if !isOpen {
		handle, err := k.wallet.Open(k.name, 0, APPID)
		if err != nil {
			return err
		}
		k.handle = handle
	}

	return nil
}

func (k *kwalletKeyring) Get(key string) (Item, error) {
	err := k.openWallet()
	if err != nil {
		return Item{}, err
	}

	data, err := k.wallet.ReadEntry(k.handle, FOLDER, key, APPID)
	if err != nil {
		return Item{}, err
	}

	item := Item{}
	err = json.Unmarshal(data, &item)
	if err != nil {
		return Item{}, err
	}

	return item, nil
}

func (k *kwalletKeyring) Set(item Item) error {
	err := k.openWallet()
	if err != nil {
		return err
	}

	data, err := json.Marshal(item)
	if err != nil {
		return err
	}

	err = k.wallet.WriteEntry(k.handle, FOLDER, item.Key, data, APPID)
	if err != nil {
		return err
	}

	return nil
}

func (k *kwalletKeyring) Remove(key string) error {
	err := k.openWallet()
	if err != nil {
		return err
	}

	err = k.wallet.RemoveEntry(k.handle, FOLDER, key, APPID)
	if err != nil {
		return err
	}

	return nil
}

func (k *kwalletKeyring) Keys() ([]string, error) {
	err := k.openWallet()
	if err != nil {
		return []string{}, err
	}

	entries, err := k.wallet.EntryList(k.handle, FOLDER, APPID)
	if err != nil {
		return []string{}, err
	}

	return entries, nil
}

func newKwallet() (*kwalletBinding, error) {
	conn, err := dbus.SessionBus()
	if err != nil {
		return nil, err
	}

	return &kwalletBinding{
		conn.Object(DBUS_SERVICE_NAME, DBUS_PATH),
	}, nil
}

// Dumb Dbus bindings for kwallet bindings with types
type kwalletBinding struct {
	dbus dbus.BusObject
}

// method bool org.kde.KWallet.isOpen(int handle)
func (k *kwalletBinding) IsOpen(handle int32) (bool, error) {
	call := k.dbus.Call("org.kde.KWallet.isOpen", 0, handle)
	if call.Err != nil {
		return false, call.Err
	}

	return call.Body[0].(bool), call.Err
}

// method int org.kde.KWallet.open(QString wallet, qlonglong wId, QString appid)
func (k *kwalletBinding) Open(name string, wID int64, appid string) (int32, error) {
	call := k.dbus.Call("org.kde.KWallet.open", 0, name, wID, appid)
	if call.Err != nil {
		return 0, call.Err
	}

	return call.Body[0].(int32), call.Err
}

// method QStringList org.kde.KWallet.entryList(int handle, QString folder, QString appid)
func (k *kwalletBinding) EntryList(handle int32, folder string, appid string) ([]string, error) {
	call := k.dbus.Call("org.kde.KWallet.entryList", 0, handle, folder, appid)
	if call.Err != nil {
		return []string{}, call.Err
	}

	return call.Body[0].([]string), call.Err
}

// method int org.kde.KWallet.writeEntry(int handle, QString folder, QString key, QByteArray value, QString appid)
func (k *kwalletBinding) WriteEntry(handle int32, folder string, key string, value []byte, appid string) error {
	call := k.dbus.Call("org.kde.KWallet.writeEntry", 0, handle, folder, key, value, appid)
	if call.Err != nil {
		return call.Err
	}

	return call.Err
}

// method int org.kde.KWallet.removeEntry(int handle, QString folder, QString key, QString appid)
func (k *kwalletBinding) RemoveEntry(handle int32, folder string, key string, appid string) error {
	call := k.dbus.Call("org.kde.KWallet.removeEntry", 0, handle, folder, key, appid)
	if call.Err != nil {
		return call.Err
	}

	return call.Err
}

// method QByteArray org.kde.KWallet.readEntry(int handle, QString folder, QString key, QString appid)
func (k *kwalletBinding) ReadEntry(handle int32, folder string, key string, appid string) ([]byte, error) {
	call := k.dbus.Call("org.kde.KWallet.readEntry", 0, handle, folder, key, appid)
	if call.Err != nil {
		return []byte{}, call.Err
	}

	return call.Body[0].([]byte), call.Err
}
<|endoftext|>