Feature done: Today's Agenda
This commit is contained in:
parent
a9d880a486
commit
113b161e0f
@ -4,16 +4,62 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
calendar "google.golang.org/api/calendar/v3"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
|
||||
calendar "google.golang.org/api/calendar/v3"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
CC *CalClient
|
||||
Service *calendar.Service
|
||||
}
|
||||
|
||||
func GetAccount(secret, token []byte) (*Account, error) {
|
||||
var err error
|
||||
a := new(Account)
|
||||
|
||||
// Create the CalClient
|
||||
a.CC = CreateClient(secret, token)
|
||||
a.CC.TokenToRaw()
|
||||
|
||||
// Create the Calendar Service
|
||||
a.Service, err = calendar.New(a.CC.client)
|
||||
if err != nil {
|
||||
return nil, errors.New("Unable to retrieve calendar Client: " + err.Error())
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (a *Account) GetTodaysEvents() []Event {
|
||||
var ret []Event
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (a *Account) GetDefaultCalendar() *Calendar {
|
||||
c, _ := state.account.Service.CalendarList.Get("primary").Do()
|
||||
return GoogleCalendarToLocal(c)
|
||||
}
|
||||
|
||||
func (a *Account) GetCalendarList() []Calendar {
|
||||
// TODO: Check if we have the calendar list cached
|
||||
var ret []Calendar
|
||||
calList, err := state.account.Service.CalendarList.List().Do()
|
||||
if err != nil {
|
||||
return ret
|
||||
}
|
||||
for _, c := range calList.Items {
|
||||
ret = append(ret, *GoogleCalendarToLocal(c))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type CalClient struct {
|
||||
rawClientSecret []byte
|
||||
rawToken []byte
|
||||
@ -72,7 +118,9 @@ func (c *CalClient) tokenFromWeb() error {
|
||||
log.Fatalf("Unable to read authorization code %v", err)
|
||||
}
|
||||
|
||||
c.tkn, err = c.cfg.Exchange(oauth2.NoContext, code)
|
||||
if c.tkn, err = c.cfg.Exchange(oauth2.NoContext, code); err == nil {
|
||||
c.TokenToRaw()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -82,3 +130,7 @@ func (c *CalClient) TokenToRaw() {
|
||||
json.NewEncoder(&tmpToken).Encode(t)
|
||||
c.rawToken = tmpToken.Bytes()
|
||||
}
|
||||
|
||||
func (c *CalClient) GetRawToken() []byte {
|
||||
return c.rawToken
|
||||
}
|
127
calendar_struct.go
Normal file
127
calendar_struct.go
Normal file
@ -0,0 +1,127 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
calendar "google.golang.org/api/calendar/v3"
|
||||
)
|
||||
|
||||
type Calendar struct {
|
||||
AccessRole string
|
||||
BackgroundColor string
|
||||
ColorId string
|
||||
DefaultReminders []*calendar.EventReminder // TODO: Local Reminder struct?
|
||||
Deleted bool
|
||||
Description string
|
||||
Etag string
|
||||
ForegroundColor string
|
||||
Hidden bool
|
||||
Id string
|
||||
Kind string
|
||||
Location string
|
||||
NotificationSettings *calendar.CalendarListEntryNotificationSettings // TODO: Needed?
|
||||
Primary bool
|
||||
Selected bool
|
||||
Summary string
|
||||
SummaryOverride string
|
||||
TimeZone string
|
||||
ForceSendFields []string // TODO: Needed?
|
||||
NullFields []string //TODO: Needed?
|
||||
}
|
||||
|
||||
func GoogleCalendarToLocal(c *calendar.CalendarListEntry) *Calendar {
|
||||
ret := Calendar{
|
||||
AccessRole: c.AccessRole,
|
||||
BackgroundColor: c.BackgroundColor,
|
||||
ColorId: c.ColorId,
|
||||
DefaultReminders: c.DefaultReminders,
|
||||
Deleted: c.Deleted,
|
||||
Description: c.Description,
|
||||
Etag: c.Etag,
|
||||
ForegroundColor: c.ForegroundColor,
|
||||
Hidden: c.Hidden,
|
||||
Id: c.Id,
|
||||
Kind: c.Kind,
|
||||
Location: c.Location,
|
||||
Primary: c.Primary,
|
||||
Selected: c.Selected,
|
||||
Summary: c.Summary,
|
||||
SummaryOverride: c.SummaryOverride,
|
||||
TimeZone: c.TimeZone,
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (c *Calendar) GetTodaysEvents() []Event {
|
||||
var ret []Event
|
||||
|
||||
minTime := time.Date(
|
||||
time.Now().Year(),
|
||||
time.Now().Month(),
|
||||
time.Now().Day(),
|
||||
0, 0, 0, 0, time.Local,
|
||||
)
|
||||
maxTime := time.Date(
|
||||
time.Now().Year(),
|
||||
time.Now().Month(),
|
||||
time.Now().Day(),
|
||||
23, 59, 59, 999999999, time.Local,
|
||||
)
|
||||
events, err := state.account.Service.Events.List(c.Id).
|
||||
ShowDeleted(false).
|
||||
SingleEvents(true).
|
||||
TimeMin(minTime.Format(time.RFC3339)).
|
||||
TimeMax(maxTime.Format(time.RFC3339)).
|
||||
OrderBy("startTime").
|
||||
Do()
|
||||
if err != nil {
|
||||
return ret
|
||||
}
|
||||
for _, e := range events.Items {
|
||||
ret = append(ret, *GoogleEventToLocal(e))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Calendar) GetCalendarEvents() []Event {
|
||||
var ret []Event
|
||||
events, err := state.account.Service.Events.List(c.Id).ShowDeleted(false).
|
||||
SingleEvents(true).OrderBy("startTime").Do()
|
||||
if err != nil {
|
||||
return ret
|
||||
}
|
||||
for _, e := range events.Items {
|
||||
ret = append(ret, *GoogleEventToLocal(e))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/* For Adding a private copy of an existing event
|
||||
func (c *Calendar) ImportEvent(event *Event) error {
|
||||
_, err := state.account.Service.Events.Import(c.Id, event).Do()
|
||||
return err
|
||||
}
|
||||
*/
|
||||
func (c *Calendar) InsertEvent(event *Event) (*Event, error) {
|
||||
e, err := state.account.Service.Events.Insert(c.Id, LocalEventToGoogle(event)).Do()
|
||||
return GoogleEventToLocal(e), err
|
||||
}
|
||||
|
||||
func (c *Calendar) DeleteEvent(eId string) error {
|
||||
return state.account.Service.Events.Delete(c.Id, eId).Do()
|
||||
}
|
||||
|
||||
func (c *Calendar) GetEvent(eId string) (*Event, error) {
|
||||
e, err := state.account.Service.Events.Get(c.Id, eId).Do()
|
||||
return GoogleEventToLocal(e), err
|
||||
}
|
||||
|
||||
func (c *Calendar) QuickAdd(text string) (*Event, error) {
|
||||
e, err := state.account.Service.Events.QuickAdd(c.Id, text).Do()
|
||||
return GoogleEventToLocal(e), err
|
||||
}
|
||||
|
||||
func (c *Calendar) Update(eId string, event *Event) (*Event, error) {
|
||||
e, err := state.account.Service.Events.Update(c.Id, eId, LocalEventToGoogle(event)).Do()
|
||||
return GoogleEventToLocal(e), err
|
||||
}
|
240
event_struct.go
Normal file
240
event_struct.go
Normal file
@ -0,0 +1,240 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
calendar "google.golang.org/api/calendar/v3"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
//Attachments []EventAttachment // Google Drive files
|
||||
Attendees []EventAttendee
|
||||
ColorId string
|
||||
Created string
|
||||
Creator *EventAttendee
|
||||
Description string
|
||||
End *calendar.EventDateTime
|
||||
EndTimeUnspecified bool
|
||||
Etag string
|
||||
//ExtendedProperties []EventExtendedProperties
|
||||
GuestsCanInviteOthers bool
|
||||
GuestsCanModify bool
|
||||
GuestsCanSeeOtherGuests bool
|
||||
HangoutLink string
|
||||
HtmlLink string
|
||||
ICalUID string
|
||||
Id string
|
||||
Kind string
|
||||
Location string
|
||||
Locked bool
|
||||
Organizer *EventAttendee
|
||||
OriginalStartTime *calendar.EventDateTime
|
||||
PrivateCopy bool
|
||||
Recurrence []string
|
||||
RecurringEventId string
|
||||
//Reminders *EventReminders
|
||||
Sequence int64
|
||||
//Source *EventSource
|
||||
Start *calendar.EventDateTime
|
||||
Status string
|
||||
Summary string
|
||||
Transparency string
|
||||
Updated string
|
||||
Visibility string
|
||||
}
|
||||
|
||||
func (e *Event) ToCLIString() string {
|
||||
return fmt.Sprintf("%s\n%s\n", e.Summary, e.GetStartTime())
|
||||
}
|
||||
|
||||
func (e *Event) GetStartTime() string {
|
||||
tm, err := time.Parse(time.RFC3339, e.Start.DateTime)
|
||||
if err != nil {
|
||||
return "00:00:00"
|
||||
}
|
||||
return tm.Local().Format("15:04:05")
|
||||
}
|
||||
|
||||
func GoogleEventToLocal(e *calendar.Event) *Event {
|
||||
return &Event{
|
||||
//Attachments []EventAttachment // Google Drive files
|
||||
Attendees: GoogleAttendeeSliceToLocal(e.Attendees),
|
||||
ColorId: e.ColorId,
|
||||
Created: e.Created,
|
||||
Creator: GoogleCreatorToLocal(e.Creator),
|
||||
Description: e.Description,
|
||||
End: e.End,
|
||||
EndTimeUnspecified: e.EndTimeUnspecified,
|
||||
Etag: e.Etag,
|
||||
//ExtendedProperties []EventExtendedProperties
|
||||
//GuestsCanInviteOthers: *e.GuestsCanInviteOthers,
|
||||
GuestsCanModify: e.GuestsCanModify,
|
||||
//GuestsCanSeeOtherGuests: *e.GuestsCanSeeOtherGuests,
|
||||
HangoutLink: e.HangoutLink,
|
||||
HtmlLink: e.HtmlLink,
|
||||
ICalUID: e.ICalUID,
|
||||
Id: e.Id,
|
||||
Kind: e.Kind,
|
||||
Location: e.Location,
|
||||
Locked: e.Locked,
|
||||
Organizer: GoogleOrganizerToLocal(e.Organizer),
|
||||
OriginalStartTime: e.OriginalStartTime,
|
||||
PrivateCopy: e.PrivateCopy,
|
||||
Recurrence: e.Recurrence,
|
||||
RecurringEventId: e.RecurringEventId,
|
||||
//Reminders *EventReminders
|
||||
Sequence: e.Sequence,
|
||||
//Source *EventSource
|
||||
Start: e.Start,
|
||||
Status: e.Status,
|
||||
Summary: e.Summary,
|
||||
Transparency: e.Transparency,
|
||||
Updated: e.Updated,
|
||||
Visibility: e.Visibility,
|
||||
}
|
||||
}
|
||||
|
||||
func LocalEventToGoogle(e *Event) *calendar.Event {
|
||||
return &calendar.Event{
|
||||
//Attachments []EventAttachment // Google Drive files
|
||||
//Attendees: GoogleAttendeeSliceToLocal(e.Attendees),
|
||||
ColorId: e.ColorId,
|
||||
Created: e.Created,
|
||||
Creator: LocalCreatorToGoogle(e.Creator),
|
||||
Description: e.Description,
|
||||
End: e.End,
|
||||
EndTimeUnspecified: e.EndTimeUnspecified,
|
||||
Etag: e.Etag,
|
||||
//ExtendedProperties []EventExtendedProperties
|
||||
//GuestsCanInviteOthers: *e.GuestsCanInviteOthers,
|
||||
GuestsCanModify: e.GuestsCanModify,
|
||||
//GuestsCanSeeOtherGuests: *e.GuestsCanSeeOtherGuests,
|
||||
HangoutLink: e.HangoutLink,
|
||||
HtmlLink: e.HtmlLink,
|
||||
ICalUID: e.ICalUID,
|
||||
Id: e.Id,
|
||||
Kind: e.Kind,
|
||||
Location: e.Location,
|
||||
Locked: e.Locked,
|
||||
Organizer: LocalOrganizerToGoogle(e.Organizer),
|
||||
OriginalStartTime: e.OriginalStartTime,
|
||||
PrivateCopy: e.PrivateCopy,
|
||||
Recurrence: e.Recurrence,
|
||||
RecurringEventId: e.RecurringEventId,
|
||||
//Reminders *EventReminders
|
||||
Sequence: e.Sequence,
|
||||
//Source *EventSource
|
||||
Start: e.Start,
|
||||
Status: e.Status,
|
||||
Summary: e.Summary,
|
||||
Transparency: e.Transparency,
|
||||
Updated: e.Updated,
|
||||
Visibility: e.Visibility,
|
||||
}
|
||||
}
|
||||
|
||||
type EventAttendee struct {
|
||||
AdditionalGuests int64
|
||||
Comment string
|
||||
DisplayName string
|
||||
Email string
|
||||
Id string
|
||||
Optional bool
|
||||
Organizer bool
|
||||
Resource bool
|
||||
ResponseStatus string
|
||||
Self bool
|
||||
EventCreator bool
|
||||
EventOrganizer bool
|
||||
}
|
||||
|
||||
func GoogleAttendeeSliceToLocal(a []*calendar.EventAttendee) []EventAttendee {
|
||||
var ret []EventAttendee
|
||||
for _, e := range a {
|
||||
ret = append(ret, *GoogleAttendeeToLocal(e))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func LocalAttendeeSliceToGoogle(a []EventAttendee) []*calendar.EventAttendee {
|
||||
var ret []*calendar.EventAttendee
|
||||
for _, e := range a {
|
||||
ret = append(ret, LocalAttendeeToGoogle(&e))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func GoogleAttendeeToLocal(a *calendar.EventAttendee) *EventAttendee {
|
||||
return &EventAttendee{
|
||||
AdditionalGuests: a.AdditionalGuests,
|
||||
Comment: a.Comment,
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Optional: a.Optional,
|
||||
Organizer: a.Organizer,
|
||||
Resource: a.Resource,
|
||||
ResponseStatus: a.ResponseStatus,
|
||||
Self: a.Self,
|
||||
}
|
||||
}
|
||||
|
||||
func LocalAttendeeToGoogle(a *EventAttendee) *calendar.EventAttendee {
|
||||
return &calendar.EventAttendee{
|
||||
AdditionalGuests: a.AdditionalGuests,
|
||||
Comment: a.Comment,
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Optional: a.Optional,
|
||||
Organizer: a.Organizer,
|
||||
Resource: a.Resource,
|
||||
ResponseStatus: a.ResponseStatus,
|
||||
Self: a.Self,
|
||||
}
|
||||
}
|
||||
|
||||
func GoogleOrganizerToLocal(a *calendar.EventOrganizer) *EventAttendee {
|
||||
return &EventAttendee{
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Self: a.Self,
|
||||
EventOrganizer: true,
|
||||
}
|
||||
}
|
||||
|
||||
func LocalOrganizerToGoogle(a *EventAttendee) *calendar.EventOrganizer {
|
||||
return &calendar.EventOrganizer{
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Self: a.Self,
|
||||
}
|
||||
}
|
||||
|
||||
func GoogleCreatorToLocal(a *calendar.EventCreator) *EventAttendee {
|
||||
return &EventAttendee{
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Self: a.Self,
|
||||
EventCreator: true,
|
||||
}
|
||||
}
|
||||
|
||||
func LocalCreatorToGoogle(a *EventAttendee) *calendar.EventCreator {
|
||||
return &calendar.EventCreator{
|
||||
DisplayName: a.DisplayName,
|
||||
Email: a.Email,
|
||||
Id: a.Id,
|
||||
Self: a.Self,
|
||||
}
|
||||
}
|
||||
|
||||
type ByStartTime []Event
|
||||
|
||||
func (a ByStartTime) Len() int { return len(a) }
|
||||
func (a ByStartTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByStartTime) Less(i, j int) bool { return a[i].Start.DateTime < a[j].Start.DateTime }
|
115
main.go
115
main.go
@ -5,9 +5,8 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
calendar "google.golang.org/api/calendar/v3"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/br0xen/user-config"
|
||||
)
|
||||
@ -22,12 +21,14 @@ type AppState struct {
|
||||
Version int
|
||||
ClientSecret []byte
|
||||
cfg *userConfig.Config
|
||||
account *Account
|
||||
}
|
||||
|
||||
var state *AppState
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var op string
|
||||
state = &AppState{Name: AppName, Version: AppVersion}
|
||||
state.cfg, err = userConfig.NewConfig(state.Name)
|
||||
|
||||
@ -35,53 +36,71 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := range os.Args {
|
||||
switch {
|
||||
case os.Args[i] == "--reinit":
|
||||
// Reset all config
|
||||
for _, v := range state.cfg.GetKeyList() {
|
||||
state.cfg.DeleteKey(v)
|
||||
}
|
||||
if len(os.Args) > 1 {
|
||||
op = os.Args[1]
|
||||
switch os.Args[1] {
|
||||
case "--reinit":
|
||||
}
|
||||
} else {
|
||||
op = "today"
|
||||
}
|
||||
|
||||
DoVersionCheck()
|
||||
|
||||
sec := state.cfg.GetBytes("ClientSecret")
|
||||
tkn := state.cfg.GetBytes("Token")
|
||||
|
||||
c := CreateClient(sec, tkn)
|
||||
|
||||
c.TokenToRaw()
|
||||
state.cfg.SetBytes("Token", c.rawToken)
|
||||
|
||||
srv, err := calendar.New(c.client)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("unable to retrieve calendar Client %v", err)
|
||||
switch op {
|
||||
case "--reinit":
|
||||
// Reset all config
|
||||
for _, v := range state.cfg.GetKeyList() {
|
||||
fmt.Println("Deleting Key: " + v)
|
||||
state.cfg.DeleteKey(v)
|
||||
}
|
||||
|
||||
listSvc := srv.CalendarList.List()
|
||||
list, err := listSvc.Do()
|
||||
if err != nil {
|
||||
log.Fatalf("unable to retrieve calendar list %v", err)
|
||||
}
|
||||
if len(list.Items) > 0 {
|
||||
for i := range list.Items {
|
||||
if list.Items[i].Deleted {
|
||||
case "today":
|
||||
// Show everything on the calendar for today
|
||||
InitComm()
|
||||
// TODO: Get calendars flagged as default
|
||||
list := state.account.GetCalendarList()
|
||||
var todayEvents []Event
|
||||
if len(list) > 0 {
|
||||
for i := range list {
|
||||
if list[i].Deleted {
|
||||
// Deleted calendar, next please
|
||||
continue
|
||||
}
|
||||
fmt.Print("(" + list.Items[i].Id + ") ")
|
||||
fmt.Print(list.Items[i].Summary + "; ")
|
||||
fmt.Println(list.Items[i].Description)
|
||||
todayEvents = append(todayEvents, list[i].GetTodaysEvents()...)
|
||||
}
|
||||
sort.Sort(ByStartTime(todayEvents))
|
||||
for _, e := range todayEvents {
|
||||
if e.GetStartTime() == "00:00:00" {
|
||||
fmt.Println("[All Day ] " + e.Summary)
|
||||
} else {
|
||||
fmt.Println("[" + e.GetStartTime() + "] " + e.Summary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("No calendars found.\n")
|
||||
}
|
||||
|
||||
fmt.Println("\n-====-\n")
|
||||
case "defaults":
|
||||
// Show Defaults
|
||||
InitComm()
|
||||
|
||||
case "add":
|
||||
// Quick event add to primary calendar
|
||||
quickAddText := strings.Join(os.Args[2:], " ")
|
||||
e, err := state.account.GetDefaultCalendar().QuickAdd(quickAddText)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
} else {
|
||||
fmt.Println(e.ToCLIString())
|
||||
}
|
||||
case "bail":
|
||||
// Just initialize communications and bail
|
||||
InitComm()
|
||||
}
|
||||
|
||||
//fmt.Println("\n-====-\n")
|
||||
|
||||
/*
|
||||
t := time.Now().Format(time.RFC3339)
|
||||
events, err := srv.Events.List("primary").ShowDeleted(false).
|
||||
SingleEvents(true).TimeMin(t).MaxResults(10).OrderBy("startTime").Do()
|
||||
@ -105,6 +124,31 @@ func main() {
|
||||
} else {
|
||||
fmt.Printf("No upcoming events found.\n")
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func InitComm() {
|
||||
var err error
|
||||
sec := state.cfg.GetBytes("ClientSecret")
|
||||
tkn := state.cfg.GetBytes("Token")
|
||||
|
||||
state.account, err = GetAccount(sec, tkn)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to get Account: %v", err)
|
||||
}
|
||||
|
||||
// Save the Raw Token
|
||||
state.cfg.SetBytes("Token", state.account.CC.GetRawToken())
|
||||
|
||||
// If the 'defaultCalendars' cfg is set to 'primary', we need to actually but the primary cal id in there
|
||||
var defCal []string
|
||||
defCal, err = state.cfg.GetArray("defaultCalendars")
|
||||
if len(defCal) == 0 || (len(defCal) == 1 && defCal[0] == "primary") {
|
||||
gCal, err := state.account.Service.CalendarList.Get("primary").Do()
|
||||
if err == nil {
|
||||
state.cfg.SetArray("defaultCalendars", []string{gCal.Id})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DoVersionCheck() {
|
||||
@ -127,6 +171,7 @@ func DoVersionCheck() {
|
||||
}
|
||||
}
|
||||
state.cfg.SetInt("version", 1)
|
||||
state.cfg.SetArray("defaultCalendars", []string{"primary"})
|
||||
}
|
||||
state.ClientSecret = state.cfg.GetBytes("ClientSecret")
|
||||
// Refetch the version from the config
|
||||
|
Loading…
Reference in New Issue
Block a user