From fcce379deacac818258415607aafc891e462e695 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 16 Jun 2018 20:52:35 +0200 Subject: [PATCH] Added Channel model and API for it --- cmd/rpdata-ensurechannels/main.go | 42 ++++++++ makefile | 1 + model/channel/channel.go | 133 ++++++++++++++++++++++++++ model/log/log.go | 12 ++- resolver/channel.go | 154 ++++++++++++++++++++++++++++++ schema/root.graphql | 17 ++++ schema/types/channel.graphql | 51 ++++++++++ 7 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 cmd/rpdata-ensurechannels/main.go create mode 100644 model/channel/channel.go create mode 100644 resolver/channel.go create mode 100644 schema/types/channel.graphql diff --git a/cmd/rpdata-ensurechannels/main.go b/cmd/rpdata-ensurechannels/main.go new file mode 100644 index 0000000..b23b13a --- /dev/null +++ b/cmd/rpdata-ensurechannels/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + + "git.aiterp.net/rpdata/api/model/channel" + + "git.aiterp.net/rpdata/api/internal/store" + "git.aiterp.net/rpdata/api/model/log" +) + +func main() { + err := store.Init() + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + + logs, err := log.List(32768) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + + added := make(map[string]bool, 1024) + for _, log := range logs { + if added[log.Channel] { + continue + } + + _, err := channel.Ensure(log.Channel, false) + if err != nil { + fmt.Fprintln(os.Stderr, log.ID, err) + continue + } + + added[log.Channel] = true + + fmt.Println(log.Channel, "ensured") + } +} diff --git a/makefile b/makefile index 430719e..425e61e 100644 --- a/makefile +++ b/makefile @@ -11,6 +11,7 @@ build: go build -ldflags="-s -w" -o $(INSTALL_PATH)/usr/bin/rpdata-lb2charimport ./cmd/rpdata-lb2charimport go build -ldflags="-s -w" -o $(INSTALL_PATH)/usr/bin/rpdata-lb2logimport ./cmd/rpdata-lb2logimport go build -ldflags="-s -w" -o $(INSTALL_PATH)/usr/bin/rpdata-wikifileimport ./cmd/rpdata-wikifileimport + go build -ldflags="-s -w" -o $(INSTALL_PATH)/usr/bin/rpdata-ensurechannels ./cmd/rpdata-ensurechannels install: cp $(INSTALL_PATH)/usr/bin/* /usr/local/bin/ \ No newline at end of file diff --git a/model/channel/channel.go b/model/channel/channel.go new file mode 100644 index 0000000..2ccf777 --- /dev/null +++ b/model/channel/channel.go @@ -0,0 +1,133 @@ +package channel + +import ( + "errors" + "strings" + + "git.aiterp.net/rpdata/api/internal/store" + "github.com/globalsign/mgo" + "github.com/globalsign/mgo/bson" +) + +var collection *mgo.Collection + +// ErrInvalidName is an error for an invalid channel name +var ErrInvalidName = errors.New("Invalid channel name") + +// A Channel represents information abount an IRC RP channel, and whether it should be logged +type Channel struct { + Name string `bson:"_id"` + Logged bool `bson:"logged"` + Hub bool `bson:"hub"` + Event string `bson:"event,omitempty"` + Location string `bson:"location,omitempty"` +} + +// Edit edits the channel +func (channel *Channel) Edit(logged, hub *bool, event, location *string) error { + changes := bson.M{} + changed := *channel + + if logged != nil && channel.Logged != *logged { + changes["logged"] = *logged + changed.Logged = *logged + } + if hub != nil && channel.Hub != *hub { + changes["hub"] = *hub + changed.Hub = *hub + } + if event != nil && channel.Event != *event { + changes["event"] = *event + changed.Event = *event + } + if location != nil && channel.Event != *location { + changes["location"] = *location + changed.Location = *location + } + + if len(changes) == 0 { + return nil + } + + err := collection.UpdateId(channel.Name, bson.M{"$set": changes}) + if err != nil { + return err + } + + *channel = changed + + return nil +} + +// Remove removes the channel information from the database. +func (channel *Channel) Remove() error { + return collection.RemoveId(channel.Name) +} + +// Ensure ensures a channel's existence. It does not change `logged` if there is +// an existing channel. +func Ensure(name string, logged bool) (Channel, error) { + channel, err := FindName(name) + if err == mgo.ErrNotFound { + return New(name, logged, false, "", "") + } else if err != nil { + return Channel{}, err + } + + return channel, nil +} + +// New creates a new channel +func New(name string, logged, hub bool, event, location string) (Channel, error) { + if len(name) < 3 && !strings.HasPrefix(name, "#") { + return Channel{}, ErrInvalidName + } + + channel := Channel{ + Name: name, + Logged: logged, + Hub: hub, + Event: event, + Location: location, + } + + err := collection.Insert(channel) + if err != nil { + return Channel{}, err + } + + return channel, nil +} + +// FindName finds a channel by its id (its name). +func FindName(name string) (Channel, error) { + channel := Channel{} + err := collection.FindId(name).One(&channel) + + return channel, err +} + +// List finds channels, if logged is true it will be limited to logged +// channels +func List(logged bool) ([]Channel, error) { + query := bson.M{} + if logged { + query["logged"] = true + } + + channels := make([]Channel, 0, 32) + err := collection.Find(query).All(&channels) + + return channels, err +} + +func init() { + store.HandleInit(func(db *mgo.Database) { + collection = db.C("common.channels") + + collection.EnsureIndexKey("logged") + collection.EnsureIndexKey("hub") + collection.EnsureIndexKey("event") + collection.EnsureIndexKey("location") + }) +} diff --git a/model/log/log.go b/model/log/log.go index 4011eef..1ae6ccf 100644 --- a/model/log/log.go +++ b/model/log/log.go @@ -11,6 +11,7 @@ import ( "time" "git.aiterp.net/rpdata/api/internal/store" + "git.aiterp.net/rpdata/api/model/channel" "git.aiterp.net/rpdata/api/model/character" "git.aiterp.net/rpdata/api/model/counter" @@ -39,17 +40,22 @@ type Log struct { } // New creates a new Log -func New(date time.Time, channel, title, event, description string, open bool) (Log, error) { +func New(date time.Time, channelName, title, event, description string, open bool) (Log, error) { nextID, err := counter.Next("auto_increment", "Log") if err != nil { return Log{}, err } + _, err = channel.Ensure(channelName, open) + if err != nil { + return Log{}, err + } + log := Log{ - ID: MakeLogID(date, channel), + ID: MakeLogID(date, channelName), ShortID: "L" + strconv.Itoa(nextID), Date: date, - Channel: channel, + Channel: channelName, Title: title, Event: event, Description: description, diff --git a/resolver/channel.go b/resolver/channel.go new file mode 100644 index 0000000..c80e1c0 --- /dev/null +++ b/resolver/channel.go @@ -0,0 +1,154 @@ +package resolver + +import ( + "context" + + "git.aiterp.net/rpdata/api/internal/session" + "git.aiterp.net/rpdata/api/model/channel" +) + +// ChannelResolver for the Channel graphql type +type ChannelResolver struct{ C channel.Channel } + +// ChannelArgs is args for channel query +type ChannelArgs struct { + Name string +} + +// Channel implements the channel query +func (r *QueryResolver) Channel(ctx context.Context, args *ChannelArgs) (*ChannelResolver, error) { + channel, err := channel.FindName(args.Name) + if err != nil { + return nil, err + } + + return &ChannelResolver{C: channel}, nil +} + +// ChannelsArgs is args for channel query +type ChannelsArgs struct { + Logged *bool +} + +// Channels implements the channels query +func (r *QueryResolver) Channels(ctx context.Context, args *ChannelsArgs) ([]*ChannelResolver, error) { + channels, err := channel.List(args.Logged != nil && *args.Logged) + if err != nil { + return nil, err + } + + resolvers := make([]*ChannelResolver, len(channels)) + for i := range channels { + resolvers[i] = &ChannelResolver{C: channels[i]} + } + + return resolvers, nil +} + +// ChannelAddEditInput is input for the addChannel mutation +type ChannelAddEditInput struct { + Name string + Logged *bool + Hub *bool + EventName *string + LocationName *string +} + +// AddChannel resolves the addChannel mutation +func (r *MutationResolver) AddChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) { + user := session.FromContext(ctx).User() + if user == nil || !user.Permitted("channel.add") { + return nil, ErrUnauthorized + } + + logged := args.Input.Logged != nil && *args.Input.Logged + hub := args.Input.Hub != nil && *args.Input.Hub + eventName := "" + if args.Input.EventName != nil { + eventName = *args.Input.EventName + } + locationName := "" + if args.Input.LocationName != nil { + locationName = *args.Input.LocationName + } + + channel, err := channel.New(args.Input.Name, logged, hub, eventName, locationName) + if err != nil { + return nil, err + } + + return &ChannelResolver{C: channel}, nil +} + +// EditChannel resolves the editChannel mutation +func (r *MutationResolver) EditChannel(ctx context.Context, args struct{ Input *ChannelAddEditInput }) (*ChannelResolver, error) { + user := session.FromContext(ctx).User() + if user == nil || !user.Permitted("channel.edit") { + return nil, ErrUnauthorized + } + + channel, err := channel.FindName(args.Input.Name) + if err != nil { + return nil, err + } + + err = channel.Edit(args.Input.Logged, args.Input.Hub, args.Input.EventName, args.Input.LocationName) + if err != nil { + return nil, err + } + + return &ChannelResolver{C: channel}, nil +} + +// RemoveChannel resolves the editChannel mutation +func (r *MutationResolver) RemoveChannel(ctx context.Context, args ChannelArgs) (*ChannelResolver, error) { + user := session.FromContext(ctx).User() + if user == nil || !user.Permitted("channel.remove") { + return nil, ErrUnauthorized + } + + channel, err := channel.FindName(args.Name) + if err != nil { + return nil, err + } + + err = channel.Remove() + if err != nil { + return nil, err + } + + return &ChannelResolver{C: channel}, nil +} + +// Name resolves channel.name +func (r *ChannelResolver) Name() string { + return r.C.Name +} + +// Logged resolves channel.logged +func (r *ChannelResolver) Logged() bool { + return r.C.Logged +} + +// Hub resolves channel.hub +func (r *ChannelResolver) Hub() bool { + return r.C.Hub +} + +// EventName resolves channel.eventName +func (r *ChannelResolver) EventName() *string { + if r.C.Event == "" { + return nil + } + + return &r.C.Event +} + +// LocationName resolves channel.locationName +func (r *ChannelResolver) LocationName() *string { + if r.C.Location == "" { + return nil + } + + return &r.C.Location +} diff --git a/schema/root.graphql b/schema/root.graphql index 0b84200..b5babe8 100644 --- a/schema/root.graphql +++ b/schema/root.graphql @@ -6,7 +6,14 @@ type Query { # Find characters by either a list of ids, nicks or an author. Only one parameter at a time characters(ids: [String!], nicks: [String!], author: String): [Character!]! + + # Find channel by name + channel(name: String!): Channel + + # Find all channels. Specifying `logged: true` restricts the search to only logged. + channels(logged: Boolean): [Channel!]! + # Find log by ID log(id: String): Log @@ -51,6 +58,16 @@ type Mutation { removeCharacter(id: String!): Character! + # Add a channel + addChannel(input: ChannelAddInput!): Channel! + + # Edit a channel + editChannel(input: ChannelEditInput!): Channel! + + # Remove a channel + removeChannel(name: String!): Channel! + + # Add a new log addLog(input: LogAddInput!): Log! diff --git a/schema/types/channel.graphql b/schema/types/channel.graphql new file mode 100644 index 0000000..fef5666 --- /dev/null +++ b/schema/types/channel.graphql @@ -0,0 +1,51 @@ +# Information about an IRC channel +type Channel { + # The channel's name + name: String! + + # Whether the channel should be logged + logged: Boolean! + + # Whether the channel is a hub channel + hub: Boolean! + + # The event name, or `null` if none is specified + eventName: String + + # The location name, or `null` if none is specified + locationName: String +} + +input ChannelAddInput { + # The channel's name + name: String! + + # Whether the channel should be logged + logged: Boolean + + # Whether the channel is a hub channel + hub: Boolean + + # The event name, or `null` if none is specified + eventName: String + + # The location name, or `null` if none is specified + locationName: String +} + +input ChannelEditInput { + # The channel's name + name: String! + + # Whether the channel should be logged + logged: Boolean + + # Whether the channel is a hub channel + hub: Boolean + + # The event name, or `null` if none is specified + eventName: String + + # The location name, or `null` if none is specified + locationName: String +} \ No newline at end of file